@gigabuddy/gadgets 0.1.8 → 0.1.10

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/index.js CHANGED
@@ -5,6 +5,7 @@ function createGadgetRenderer(componentCode, data = {}, options) {
5
5
  const stateEnabled = options?.stateEnabled ?? false;
6
6
  const chatEnabled = options?.chatEnabled ?? false;
7
7
  const contextEnabled = options?.contextEnabled ?? false;
8
+ const roomEnabled = options?.roomEnabled ?? false;
8
9
  const escapedCode = componentCode.replace(/<\/script>/g, "<\\/script>");
9
10
  const serializedAssets = JSON.stringify(options?.assets ?? {});
10
11
  const stateBridgeScript = stateEnabled ? `
@@ -150,10 +151,183 @@ function createGadgetRenderer(componentCode, data = {}, options) {
150
151
  window.__rerenderGadget();
151
152
  }
152
153
  });` : "";
154
+ const roomBridgeScript = roomEnabled ? `
155
+ // \u2500\u2500 Room presence \u2500\u2500
156
+ window.gadget.room = {
157
+ peers: [],
158
+ userId: null,
159
+ displayName: null,
160
+ _peersCallbacks: [],
161
+ _playerJoinedCallbacks: [],
162
+ _playerLeftCallbacks: [],
163
+
164
+ setCursor: function(pos) {
165
+ window.parent.postMessage({ type: 'gadget-presence', cursor: pos }, '*');
166
+ },
167
+ setSelection: function(sel) {
168
+ window.parent.postMessage({ type: 'gadget-presence', selection: sel }, '*');
169
+ },
170
+ onPeersChange: function(cb) {
171
+ window.gadget.room._peersCallbacks.push(cb);
172
+ cb(window.gadget.room.peers);
173
+ return function() {
174
+ var i = window.gadget.room._peersCallbacks.indexOf(cb);
175
+ if (i !== -1) window.gadget.room._peersCallbacks.splice(i, 1);
176
+ };
177
+ },
178
+ onPlayerJoined: function(cb) {
179
+ window.gadget.room._playerJoinedCallbacks.push(cb);
180
+ return function() {
181
+ var i = window.gadget.room._playerJoinedCallbacks.indexOf(cb);
182
+ if (i !== -1) window.gadget.room._playerJoinedCallbacks.splice(i, 1);
183
+ };
184
+ },
185
+ onPlayerLeft: function(cb) {
186
+ window.gadget.room._playerLeftCallbacks.push(cb);
187
+ return function() {
188
+ var i = window.gadget.room._playerLeftCallbacks.indexOf(cb);
189
+ if (i !== -1) window.gadget.room._playerLeftCallbacks.splice(i, 1);
190
+ };
191
+ },
192
+ };
193
+
194
+ // \u2500\u2500 Room chat \u2500\u2500
195
+ window.gadget.roomChat = {
196
+ messages: [],
197
+ _messageCallbacks: [],
198
+
199
+ send: function(text) {
200
+ window.parent.postMessage({ type: 'gadget-chat-send', text: text }, '*');
201
+ },
202
+ onMessage: function(cb) {
203
+ window.gadget.roomChat._messageCallbacks.push(cb);
204
+ return function() {
205
+ var i = window.gadget.roomChat._messageCallbacks.indexOf(cb);
206
+ if (i !== -1) window.gadget.roomChat._messageCallbacks.splice(i, 1);
207
+ };
208
+ },
209
+ };
210
+
211
+ // \u2500\u2500 Gestures \u2500\u2500
212
+ window.gadget.gestures = {
213
+ active: [],
214
+ _spawnedCallbacks: [],
215
+ _dismissedCallbacks: [],
216
+
217
+ spawn: function(gadgetId, anchor, opts) {
218
+ opts = opts || {};
219
+ window.parent.postMessage({
220
+ type: 'gadget-gesture-send',
221
+ gadgetId: gadgetId,
222
+ anchor: anchor,
223
+ ttl: opts.ttl,
224
+ size: opts.size,
225
+ rotation: opts.rotation,
226
+ }, '*');
227
+ },
228
+ dismiss: function(gestureId) {
229
+ window.parent.postMessage({ type: 'gadget-gesture-dismiss', gestureId: gestureId }, '*');
230
+ },
231
+ reportAnchorRect: function(selector, rect) {
232
+ window.parent.postMessage({ type: 'gadget-anchor-rect', selector: selector, rect: rect }, '*');
233
+ },
234
+ onSpawned: function(cb) {
235
+ window.gadget.gestures._spawnedCallbacks.push(cb);
236
+ return function() {
237
+ var i = window.gadget.gestures._spawnedCallbacks.indexOf(cb);
238
+ if (i !== -1) window.gadget.gestures._spawnedCallbacks.splice(i, 1);
239
+ };
240
+ },
241
+ onDismissed: function(cb) {
242
+ window.gadget.gestures._dismissedCallbacks.push(cb);
243
+ return function() {
244
+ var i = window.gadget.gestures._dismissedCallbacks.indexOf(cb);
245
+ if (i !== -1) window.gadget.gestures._dismissedCallbacks.splice(i, 1);
246
+ };
247
+ },
248
+ };
249
+
250
+ // \u2500\u2500 Buddy attraction \u2500\u2500
251
+ window.gadget.buddy = {
252
+ attract: function(anchor, interest) {
253
+ window.parent.postMessage({
254
+ type: 'gadget-buddy-attract',
255
+ anchor: anchor,
256
+ interest: interest || 'medium',
257
+ }, '*');
258
+ },
259
+ };
260
+
261
+ window.addEventListener('message', function(event) {
262
+ var d = event.data;
263
+ if (!d) return;
264
+
265
+ // Presence updates
266
+ if (d.type === 'gadget-presence-update') {
267
+ window.gadget.room.peers = d.peers || [];
268
+ window.gadget.room._peersCallbacks.forEach(function(cb) { try { cb(d.peers || []); } catch(e) {} });
269
+ window.__rerenderGadget && window.__rerenderGadget();
270
+ }
271
+
272
+ // Player join/leave
273
+ if (d.type === 'gadget-player-joined') {
274
+ window.gadget.room._playerJoinedCallbacks.forEach(function(cb) { try { cb(d); } catch(e) {} });
275
+ }
276
+ if (d.type === 'gadget-player-left') {
277
+ window.gadget.room.peers = window.gadget.room.peers.filter(function(p) { return p.userId !== d.userId; });
278
+ window.gadget.room._playerLeftCallbacks.forEach(function(cb) { try { cb(d.userId); } catch(e) {} });
279
+ window.__rerenderGadget && window.__rerenderGadget();
280
+ }
281
+
282
+ // Chat
283
+ if (d.type === 'gadget-chat-message') {
284
+ var msg = d.message || d;
285
+ window.gadget.roomChat.messages.push(msg);
286
+ if (window.gadget.roomChat.messages.length > 100) window.gadget.roomChat.messages.shift();
287
+ window.gadget.roomChat._messageCallbacks.forEach(function(cb) { try { cb(msg); } catch(e) {} });
288
+ window.__rerenderGadget && window.__rerenderGadget();
289
+ }
290
+ if (d.type === 'gadget-chat-history') {
291
+ window.gadget.roomChat.messages = d.messages || [];
292
+ window.__rerenderGadget && window.__rerenderGadget();
293
+ }
294
+
295
+ // Gestures
296
+ if (d.type === 'gadget-gesture-spawned') {
297
+ window.gadget.gestures.active.push(d.gesture);
298
+ window.gadget.gestures._spawnedCallbacks.forEach(function(cb) { try { cb(d.gesture); } catch(e) {} });
299
+ window.__rerenderGadget && window.__rerenderGadget();
300
+ }
301
+ if (d.type === 'gadget-gesture-dismissed') {
302
+ window.gadget.gestures.active = window.gadget.gestures.active.filter(function(g) { return g.id !== d.gestureId; });
303
+ window.gadget.gestures._dismissedCallbacks.forEach(function(cb) { try { cb(d.gestureId); } catch(e) {} });
304
+ window.__rerenderGadget && window.__rerenderGadget();
305
+ }
306
+ if (d.type === 'gadget-gesture-sync') {
307
+ window.gadget.gestures.active = d.gestures || [];
308
+ window.__rerenderGadget && window.__rerenderGadget();
309
+ }
310
+
311
+ // Anchor rect request from host
312
+ if (d.type === 'gadget-request-anchor-rect' && d.selector) {
313
+ try {
314
+ var el = document.querySelector(d.selector);
315
+ if (el) {
316
+ var rect = el.getBoundingClientRect();
317
+ window.parent.postMessage({
318
+ type: 'gadget-anchor-rect',
319
+ selector: d.selector,
320
+ rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
321
+ }, '*');
322
+ }
323
+ } catch(e) {}
324
+ }
325
+ });` : "";
153
326
  const chatPropFragment = chatEnabled ? ", chat: window.gadget.chat" : "";
154
327
  const contextPropFragment = contextEnabled ? ", context: window.gadget.context" : "";
328
+ const roomPropFragment = roomEnabled ? ", room: window.gadget.room, roomChat: window.gadget.roomChat, gestures: window.gadget.gestures" : "";
155
329
  const breakoutPropFragment = ", breakout: { active: window.gadget.breakout._active, originalRect: window.gadget.breakout._originalRect, request: window.gadget.breakout.request, exit: window.gadget.breakout.exit }";
156
- const componentProps = stateEnabled ? `{ data: window.__GADGET_DATA__, viewport: window.__GADGET_VIEWPORT__, state: window.gadget.state ? { shared: window.gadget.state.shared, user: window.gadget.state.user } : undefined, userId: window.gadget.state ? window.gadget.state.userId : undefined${chatPropFragment}${contextPropFragment}${breakoutPropFragment} }` : `{ data: window.__GADGET_DATA__, viewport: window.__GADGET_VIEWPORT__${chatPropFragment}${contextPropFragment}${breakoutPropFragment} }`;
330
+ const componentProps = stateEnabled ? `{ data: window.__GADGET_DATA__, viewport: window.__GADGET_VIEWPORT__, state: window.gadget.state ? { shared: window.gadget.state.shared, user: window.gadget.state.user } : undefined, userId: window.gadget.state ? window.gadget.state.userId : undefined${chatPropFragment}${contextPropFragment}${breakoutPropFragment}${roomPropFragment} }` : `{ data: window.__GADGET_DATA__, viewport: window.__GADGET_VIEWPORT__${chatPropFragment}${contextPropFragment}${breakoutPropFragment}${roomPropFragment} }`;
157
331
  return `<!DOCTYPE html>
158
332
  <html lang="en">
159
333
  <head>
@@ -236,6 +410,7 @@ function createGadgetRenderer(componentCode, data = {}, options) {
236
410
  ${stateBridgeScript}
237
411
  ${chatBridgeScript}
238
412
  ${contextBridgeScript}
413
+ ${roomBridgeScript}
239
414
 
240
415
  window.__gadgetRoot = null;
241
416
  window.__gadgetComponent = null;
@@ -317,7 +492,7 @@ ${contextBridgeScript}
317
492
  window.__gadgetRoot = root;
318
493
  root.render(
319
494
  <ErrorBoundary>
320
- <ComponentToRender data={window.__GADGET_DATA__} viewport={window.__GADGET_VIEWPORT__}${stateEnabled ? " state={window.gadget.state ? { shared: window.gadget.state.shared, user: window.gadget.state.user } : undefined} userId={window.gadget.state ? window.gadget.state.userId : undefined}" : ""}${chatEnabled ? " chat={window.gadget.chat}" : ""}${contextEnabled ? " context={window.gadget.context}" : ""} breakout={{ active: window.gadget.breakout._active, originalRect: window.gadget.breakout._originalRect, request: window.gadget.breakout.request, exit: window.gadget.breakout.exit }} />
495
+ <ComponentToRender data={window.__GADGET_DATA__} viewport={window.__GADGET_VIEWPORT__}${stateEnabled ? " state={window.gadget.state ? { shared: window.gadget.state.shared, user: window.gadget.state.user } : undefined} userId={window.gadget.state ? window.gadget.state.userId : undefined}" : ""}${chatEnabled ? " chat={window.gadget.chat}" : ""}${contextEnabled ? " context={window.gadget.context}" : ""}${roomEnabled ? " room={window.gadget.room} roomChat={window.gadget.roomChat} gestures={window.gadget.gestures}" : ""} breakout={{ active: window.gadget.breakout._active, originalRect: window.gadget.breakout._originalRect, request: window.gadget.breakout.request, exit: window.gadget.breakout.exit }} />
321
496
  </ErrorBoundary>
322
497
  );
323
498
  } else {
@@ -497,8 +672,295 @@ function setupGadgetBreakout(iframe, options) {
497
672
  window.removeEventListener("message", handler);
498
673
  };
499
674
  }
675
+
676
+ // libs/gadgets/src/lib/setupGadgetAwareness.ts
677
+ function setupGadgetAwareness(iframe, options) {
678
+ const {
679
+ gadgetId,
680
+ sceneGraph,
681
+ getAwarenessStates,
682
+ onAwarenessPublish,
683
+ onSceneGraphChange,
684
+ throttleMs = 100
685
+ } = options;
686
+ let filter = "focused";
687
+ let throttleTimer = null;
688
+ let pendingUpdate = false;
689
+ function getIframeBounds() {
690
+ return iframe.getBoundingClientRect();
691
+ }
692
+ function roomToGadget(rx, ry, bounds) {
693
+ const gx = (rx - bounds.left) / bounds.width;
694
+ const gy = (ry - bounds.top) / bounds.height;
695
+ if (gx < 0 || gx > 1 || gy < 0 || gy > 1)
696
+ return null;
697
+ return { x: gx, y: gy };
698
+ }
699
+ function gadgetToRoom(gx, gy, bounds) {
700
+ return {
701
+ x: gx * bounds.width + bounds.left,
702
+ y: gy * bounds.height + bounds.top
703
+ };
704
+ }
705
+ function buildAwarenessState() {
706
+ const states = getAwarenessStates();
707
+ const bounds = getIframeBounds();
708
+ const allParticipants = [];
709
+ const focused = [];
710
+ for (const [, entry] of states) {
711
+ const s = entry.state;
712
+ if (!s || typeof s !== "object")
713
+ continue;
714
+ const participant = {
715
+ actorId: s["actorId"] ?? s["displayName"] ?? "unknown",
716
+ displayName: s["displayName"],
717
+ intent: s["intent"],
718
+ focus: s["focus"]
719
+ };
720
+ const cursorVal = s["cursor"];
721
+ if (cursorVal && typeof cursorVal === "object") {
722
+ const rc = cursorVal;
723
+ const gc = roomToGadget(rc.x, rc.y, bounds);
724
+ if (gc) {
725
+ participant.cursor = gc;
726
+ }
727
+ }
728
+ allParticipants.push(participant);
729
+ const focusVal = s["focus"];
730
+ if (focusVal === gadgetId || focusVal === `gadget:${gadgetId}`) {
731
+ focused.push(participant);
732
+ }
733
+ }
734
+ const gadgetAnchors = sceneGraph.getGadgetAnchors(gadgetId).map((a) => ({
735
+ id: a.anchorId,
736
+ bounds: a.bounds,
737
+ occupant: a.occupant,
738
+ attention: a.attention
739
+ }));
740
+ return {
741
+ participants: filter === "focused" ? focused : allParticipants,
742
+ anchors: gadgetAnchors,
743
+ focused
744
+ };
745
+ }
746
+ function scheduleForward() {
747
+ if (throttleTimer) {
748
+ pendingUpdate = true;
749
+ return;
750
+ }
751
+ forwardAwareness();
752
+ throttleTimer = setTimeout(() => {
753
+ throttleTimer = null;
754
+ if (pendingUpdate) {
755
+ pendingUpdate = false;
756
+ forwardAwareness();
757
+ }
758
+ }, throttleMs);
759
+ }
760
+ function forwardAwareness() {
761
+ if (!iframe.contentWindow)
762
+ return;
763
+ const state = buildAwarenessState();
764
+ iframe.contentWindow.postMessage({ type: "gadget-awareness-state", ...state }, "*");
765
+ }
766
+ function handleMessage(event) {
767
+ if (event.source !== iframe.contentWindow)
768
+ return;
769
+ const msg = event.data;
770
+ if (!msg || typeof msg.type !== "string")
771
+ return;
772
+ switch (msg.type) {
773
+ case "gadget-anchor-register": {
774
+ const anchors = msg.anchors;
775
+ if (Array.isArray(anchors)) {
776
+ sceneGraph.registerAnchors(gadgetId, anchors);
777
+ onSceneGraphChange?.();
778
+ }
779
+ break;
780
+ }
781
+ case "gadget-anchor-update": {
782
+ const { id, occupant, attention } = msg;
783
+ if (typeof id === "string") {
784
+ sceneGraph.updateAnchor(gadgetId, id, { occupant, attention });
785
+ onSceneGraphChange?.();
786
+ }
787
+ break;
788
+ }
789
+ case "gadget-awareness-publish": {
790
+ const state = msg.state;
791
+ if (state && typeof state === "object") {
792
+ if (state["cursor"] && typeof state["cursor"] === "object") {
793
+ const gc = state["cursor"];
794
+ const bounds = getIframeBounds();
795
+ state["cursor"] = gadgetToRoom(gc.x, gc.y, bounds);
796
+ }
797
+ onAwarenessPublish?.(gadgetId, state);
798
+ }
799
+ break;
800
+ }
801
+ case "gadget-awareness-subscribe": {
802
+ const newFilter = msg.filter;
803
+ if (newFilter === "focused" || newFilter === "room" || newFilter === "all") {
804
+ filter = newFilter;
805
+ forwardAwareness();
806
+ }
807
+ break;
808
+ }
809
+ }
810
+ }
811
+ window.addEventListener("message", handleMessage);
812
+ const onLoad = () => forwardAwareness();
813
+ iframe.addEventListener("load", onLoad);
814
+ return () => {
815
+ window.removeEventListener("message", handleMessage);
816
+ iframe.removeEventListener("load", onLoad);
817
+ if (throttleTimer)
818
+ clearTimeout(throttleTimer);
819
+ sceneGraph.removeGadget(gadgetId);
820
+ onSceneGraphChange?.();
821
+ };
822
+ }
823
+
824
+ // libs/gadgets/src/lib/sceneGraph.ts
825
+ var SceneGraph = class {
826
+ anchors = /* @__PURE__ */ new Map();
827
+ onChange = null;
828
+ /**
829
+ * Set a callback for when the scene graph changes.
830
+ */
831
+ setOnChange(cb) {
832
+ this.onChange = cb;
833
+ }
834
+ /**
835
+ * Register anchors for a gadget. Replaces any existing anchors
836
+ * from the same gadget with the same IDs.
837
+ */
838
+ registerAnchors(gadgetId, anchors) {
839
+ for (const a of anchors) {
840
+ const qualifiedId = `${gadgetId}:${a.id}`;
841
+ const parentQualified = a.parent ? `${gadgetId}:${a.parent}` : void 0;
842
+ this.anchors.set(qualifiedId, {
843
+ qualifiedId,
844
+ anchorId: a.id,
845
+ gadgetId,
846
+ bounds: a.bounds,
847
+ parent: parentQualified,
848
+ occupant: this.anchors.get(qualifiedId)?.occupant,
849
+ attention: this.anchors.get(qualifiedId)?.attention ?? []
850
+ });
851
+ }
852
+ this.onChange?.();
853
+ }
854
+ /**
855
+ * Update an anchor's occupancy or attention.
856
+ */
857
+ updateAnchor(gadgetId, anchorId, update) {
858
+ const qualifiedId = `${gadgetId}:${anchorId}`;
859
+ const anchor = this.anchors.get(qualifiedId);
860
+ if (!anchor)
861
+ return;
862
+ if (update.occupant !== void 0) {
863
+ anchor.occupant = update.occupant ?? void 0;
864
+ }
865
+ if (update.attention !== void 0) {
866
+ anchor.attention = update.attention;
867
+ }
868
+ this.onChange?.();
869
+ }
870
+ /**
871
+ * Remove all anchors for a gadget (cleanup on iframe removal).
872
+ */
873
+ removeGadget(gadgetId) {
874
+ const toRemove = [];
875
+ for (const [id, anchor] of this.anchors) {
876
+ if (anchor.gadgetId === gadgetId) {
877
+ toRemove.push(id);
878
+ }
879
+ }
880
+ for (const id of toRemove) {
881
+ this.anchors.delete(id);
882
+ }
883
+ if (toRemove.length > 0)
884
+ this.onChange?.();
885
+ }
886
+ /**
887
+ * Get all anchors as a flat map.
888
+ */
889
+ getAnchors() {
890
+ return this.anchors;
891
+ }
892
+ /**
893
+ * Get anchors for a specific gadget.
894
+ */
895
+ getGadgetAnchors(gadgetId) {
896
+ const result = [];
897
+ for (const anchor of this.anchors.values()) {
898
+ if (anchor.gadgetId === gadgetId) {
899
+ result.push(anchor);
900
+ }
901
+ }
902
+ return result;
903
+ }
904
+ /**
905
+ * Get the anchor tree, optionally rooted at a specific anchor.
906
+ */
907
+ getAnchorTree(rootId) {
908
+ const childMap = /* @__PURE__ */ new Map();
909
+ for (const anchor of this.anchors.values()) {
910
+ const parentKey = anchor.parent ?? void 0;
911
+ if (!childMap.has(parentKey))
912
+ childMap.set(parentKey, []);
913
+ childMap.get(parentKey).push(anchor);
914
+ }
915
+ const buildNode = (anchor) => {
916
+ const children = (childMap.get(anchor.qualifiedId) ?? []).map(buildNode);
917
+ return { ...anchor, children };
918
+ };
919
+ if (rootId) {
920
+ const root = this.anchors.get(rootId);
921
+ if (!root)
922
+ return [];
923
+ return [buildNode(root)];
924
+ }
925
+ return (childMap.get(void 0) ?? []).map(buildNode);
926
+ }
927
+ /**
928
+ * Find the anchor at a given point (in room coordinates).
929
+ * Returns the deepest (most specific) anchor containing the point.
930
+ */
931
+ findAnchorAt(x, y) {
932
+ let best = null;
933
+ let bestArea = Infinity;
934
+ for (const anchor of this.anchors.values()) {
935
+ const b = anchor.bounds;
936
+ if (x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) {
937
+ const area = b.w * b.h;
938
+ if (area < bestArea) {
939
+ best = anchor;
940
+ bestArea = area;
941
+ }
942
+ }
943
+ }
944
+ return best;
945
+ }
946
+ /**
947
+ * Get a serializable snapshot of all anchors.
948
+ */
949
+ toJSON() {
950
+ return Array.from(this.anchors.values()).map((a) => ({
951
+ id: a.qualifiedId,
952
+ gadgetId: a.gadgetId,
953
+ bounds: a.bounds,
954
+ parent: a.parent,
955
+ occupant: a.occupant,
956
+ attention: a.attention
957
+ }));
958
+ }
959
+ };
500
960
  export {
961
+ SceneGraph,
501
962
  createGadgetRenderer,
963
+ setupGadgetAwareness,
502
964
  setupGadgetBreakout
503
965
  };
504
966
  //# sourceMappingURL=index.js.map