@foldkit/devtools 0.113.1 → 0.114.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/overlay.d.ts CHANGED
@@ -15,7 +15,7 @@ declare const StoreService_base: Context.ServiceClass<StoreService, "foldkit/Dev
15
15
  changedPaths: HashSet.HashSet<string>;
16
16
  affectedPaths: HashSet.HashSet<string>;
17
17
  }>>;
18
- jumpTo: (index: number) => Effect.Effect<void>;
18
+ jumpTo: (index: number) => Effect.Effect<unknown>;
19
19
  resume: Effect.Effect<void>;
20
20
  clear: Effect.Effect<void>;
21
21
  stateRef: SubscriptionRef.SubscriptionRef<StoreState>;
@@ -31,10 +31,14 @@ export declare const LockScroll: Command.CommandDefinitionNoArgs<"LockScroll", E
31
31
  export declare const UnlockScroll: Command.CommandDefinitionNoArgs<"UnlockScroll", Effect.Effect<{
32
32
  readonly _tag: "UnlockedScroll";
33
33
  }, never, never>>;
34
- export declare const JumpTo: Command.CommandDefinitionWithArgs<"JumpTo", {
34
+ export declare const JumpToAndInspect: Command.CommandDefinitionWithArgs<"JumpToAndInspect", {
35
35
  index: S.Number;
36
36
  }, Effect.Effect<{
37
- readonly _tag: "CompletedJump";
37
+ readonly _tag: "ReceivedInspectedState";
38
+ readonly model: unknown;
39
+ readonly maybeMessage: Option.Option<unknown>;
40
+ readonly changedPaths: HashSet.HashSet<string>;
41
+ readonly affectedPaths: HashSet.HashSet<string>;
38
42
  }, never, StoreService>>;
39
43
  export declare const InspectState: Command.CommandDefinitionWithArgs<"InspectState", {
40
44
  index: S.Number;
@@ -1 +1 @@
1
- {"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../src/overlay.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,OAAO,EACP,MAAM,EAGN,OAAO,EAGP,MAAM,EAKN,MAAM,IAAI,CAAC,EAGX,eAAe,EAEhB,MAAM,QAAQ,CAAA;AACf,OAAO,KAAK,OAAO,MAAM,iBAAiB,CAAA;AAC1C,OAAO,EACL,KAAK,aAAa,EAElB,KAAK,aAAa,EAGlB,KAAK,WAAW,EAChB,KAAK,UAAU,EAKhB,MAAM,uBAAuB,CAAA;AAW9B,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;;oFA2D/B,CAAC;;;;;;;;;;;;;;;;;AAmPvC,cAAM,YAAa,SAAQ,iBAE1B;CAAG;;AAEJ,cAAM,iBAAkB,SAAQ,sBAGC;CAAG;AAEpC,eAAO,MAAM,UAAU;;iBAGsB,CAAA;AAE7C,eAAO,MAAM,YAAY;;iBAGwB,CAAA;AAWjD,eAAO,MAAM,MAAM;;;;wBAUlB,CAAA;AAED,eAAO,MAAM,YAAY;;;;;;;;wBAIqB,CAAA;AAE9C,eAAO,MAAM,aAAa;;;;;;wBASzB,CAAA;AAED,eAAO,MAAM,MAAM;;wBASlB,CAAA;AAED,eAAO,MAAM,KAAK;;wBASjB,CAAA;AAED,eAAO,MAAM,WAAW;;6BAYvB,CAAA;AAwsDD,eAAO,MAAM,aAAa,GACxB,OAAO,aAAa,EACpB,UAAU,gBAAgB,EAC1B,MAAM,YAAY,EAClB,aAAa,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,6DAuEhC,CAAA"}
1
+ {"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../src/overlay.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,OAAO,EACP,MAAM,EAGN,OAAO,EAGP,MAAM,EAKN,MAAM,IAAI,CAAC,EAGX,eAAe,EAEhB,MAAM,QAAQ,CAAA;AACf,OAAO,KAAK,OAAO,MAAM,iBAAiB,CAAA;AAC1C,OAAO,EACL,KAAK,aAAa,EAElB,KAAK,aAAa,EAGlB,KAAK,WAAW,EAChB,KAAK,UAAU,EAKhB,MAAM,uBAAuB,CAAA;AAW9B,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;;oFA2D/B,CAAC;;;;;;;;;;;;;;;;;AAoPvC,cAAM,YAAa,SAAQ,iBAE1B;CAAG;;AAEJ,cAAM,iBAAkB,SAAQ,sBAGC;CAAG;AAEpC,eAAO,MAAM,UAAU;;iBAGsB,CAAA;AAE7C,eAAO,MAAM,YAAY;;iBAGwB,CAAA;AAuBjD,eAAO,MAAM,gBAAgB;;;;;;;;wBAU5B,CAAA;AAED,eAAO,MAAM,YAAY;;;;;;;;wBAIqB,CAAA;AAE9C,eAAO,MAAM,aAAa;;;;;;wBASzB,CAAA;AAED,eAAO,MAAM,MAAM;;wBASlB,CAAA;AAED,eAAO,MAAM,KAAK;;wBASjB,CAAA;AAED,eAAO,MAAM,WAAW;;6BAYvB,CAAA;AA0tDD,eAAO,MAAM,aAAa,GACxB,OAAO,aAAa,EACpB,UAAU,gBAAgB,EAC1B,MAAM,YAAY,EAClB,aAAa,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,6DAwEhC,CAAA"}
package/dist/overlay.js CHANGED
@@ -81,6 +81,7 @@ const Model = S.Struct({
81
81
  expandedPaths: S.HashSet(S.String),
82
82
  changedPaths: S.HashSet(S.String),
83
83
  affectedPaths: S.HashSet(S.String),
84
+ maybePendingScrubIndex: S.Option(S.Number),
84
85
  inspectorTabs: InspectorTabsModel,
85
86
  // NOTE: empirically, inlining `Slider.Model` here throws
86
87
  // "Cannot read properties of undefined (reading 'ast')" when running slider
@@ -106,7 +107,6 @@ const ClickedToggle = m('ClickedToggle');
106
107
  const ClickedRow = m('ClickedRow', { index: S.Number });
107
108
  const ClickedResume = m('ClickedResume');
108
109
  const ClickedClear = m('ClickedClear');
109
- const CompletedJump = m('CompletedJump');
110
110
  const CompletedResume = m('CompletedResume');
111
111
  const ClickedFollowLatest = m('ClickedFollowLatest');
112
112
  const ClickedScrollToTopPill = m('ClickedScrollToTopPill');
@@ -125,6 +125,7 @@ const ReceivedInspectedState = m('ReceivedInspectedState', {
125
125
  affectedPaths: S.HashSet(S.String),
126
126
  });
127
127
  const ToggledTreeNode = m('ToggledTreeNode', { path: S.String });
128
+ const TickedScrubFrame = m('TickedScrubFrame');
128
129
  const GotInspectorTabsMessage = m('GotInspectorTabsMessage', {
129
130
  message: S.Unknown,
130
131
  });
@@ -151,7 +152,6 @@ const Message = S.Union([
151
152
  ClickedFollowLatest,
152
153
  ClickedScrollToTopPill,
153
154
  ScrolledMessageList,
154
- CompletedJump,
155
155
  CompletedResume,
156
156
  CompletedClear,
157
157
  LockedScroll,
@@ -160,6 +160,7 @@ const Message = S.Union([
160
160
  CrossedMobileBreakpoint,
161
161
  ReceivedInspectedState,
162
162
  ToggledTreeNode,
163
+ TickedScrubFrame,
163
164
  GotInspectorTabsMessage,
164
165
  ReceivedStoreUpdate,
165
166
  GotSubmodelFilterMessage,
@@ -232,17 +233,27 @@ class ShadowRootService extends Context.Service()('foldkit/DevToolsShadowRoot')
232
233
  }
233
234
  export const LockScroll = Command.define('LockScroll', LockedScroll)(lockScroll.pipe(Effect.as(LockedScroll())));
234
235
  export const UnlockScroll = Command.define('UnlockScroll', UnlockedScroll)(unlockScroll.pipe(Effect.as(UnlockedScroll())));
235
- const buildInspectionEffect = (index) => Effect.gen(function* () {
236
+ const buildInspectionFromModel = (index, model) => Effect.gen(function* () {
236
237
  const store = yield* StoreService;
237
- const model = yield* store.getModelAtIndex(index);
238
238
  const maybeMessage = yield* store.getMessageAtIndex(index);
239
239
  const diff = yield* store.getDiffAtIndex(index);
240
240
  return ReceivedInspectedState({ model, maybeMessage, ...diff });
241
241
  });
242
- export const JumpTo = Command.define('JumpTo', { index: S.Number }, CompletedJump)(({ index }) => Effect.gen(function* () {
242
+ const buildInspectionEffect = (index) => Effect.gen(function* () {
243
+ const store = yield* StoreService;
244
+ const model = yield* store.getModelAtIndex(index);
245
+ return yield* buildInspectionFromModel(index, model);
246
+ });
247
+ // NOTE: jump and inspect both need the model at `index`. Resolving it twice
248
+ // (once to render the host, once to feed the inspector) replays the segment
249
+ // from the nearest keyframe twice for a mid-segment jump. `store.jumpTo`
250
+ // returns the model it resolved so the inspector reuses that single
251
+ // resolution. Inspect-only navigation (no host pause) still uses
252
+ // `InspectState`, which resolves once on its own.
253
+ export const JumpToAndInspect = Command.define('JumpToAndInspect', { index: S.Number }, ReceivedInspectedState)(({ index }) => Effect.gen(function* () {
243
254
  const store = yield* StoreService;
244
- yield* store.jumpTo(index);
245
- return CompletedJump();
255
+ const model = yield* store.jumpTo(index);
256
+ return yield* buildInspectionFromModel(index, model);
246
257
  }));
247
258
  export const InspectState = Command.define('InspectState', { index: S.Number }, ReceivedInspectedState)(({ index }) => buildInspectionEffect(index));
248
259
  export const InspectLatest = Command.define('InspectLatest', ReceivedInspectedState)(Effect.gen(function* () {
@@ -274,7 +285,7 @@ const makeUpdate = (store, shadow, mode) => {
274
285
  const resume = Command.mapEffect(Resume(), provideContext);
275
286
  const clear = Command.mapEffect(Clear(), provideContext);
276
287
  const scrollToTop = Command.mapEffect(ScrollToTop(), provideContext);
277
- const jumpTo = (index) => Command.mapEffect(JumpTo({ index }), provideContext);
288
+ const jumpToAndInspect = (index) => Command.mapEffect(JumpToAndInspect({ index }), provideContext);
278
289
  const inspectState = (index) => Command.mapEffect(InspectState({ index }), provideContext);
279
290
  const toggleScrollLock = (shouldLock) => shouldLock ? LockScroll() : UnlockScroll();
280
291
  return (model, message) => M.value(message).pipe(M.withReturnType(), M.tags({
@@ -289,10 +300,7 @@ const makeUpdate = (store, shadow, mode) => {
289
300
  evo(model, { isMobile: () => isMobile }),
290
301
  OptionExt.when(model.isOpen, toggleScrollLock(isMobile)).pipe(Option.toArray),
291
302
  ],
292
- ClickedRow: ({ index }) => M.value(mode).pipe(M.withReturnType(), M.when('TimeTravel', () => [
293
- model,
294
- [jumpTo(index), inspectState(index)],
295
- ]), M.when('Inspect', () => [
303
+ ClickedRow: ({ index }) => M.value(mode).pipe(M.withReturnType(), M.when('TimeTravel', () => [model, [jumpToAndInspect(index)]]), M.when('Inspect', () => [
296
304
  evo(model, {
297
305
  selectedIndex: () => index,
298
306
  isFollowingLatest: () => false,
@@ -438,21 +446,34 @@ const makeUpdate = (store, shadow, mode) => {
438
446
  GotScrubberSliderMessage: ({ message: sliderMessage }) => {
439
447
  const [nextSlider, sliderCommands, maybeOutMessage] = Slider.update(model.scrubberSlider, sliderMessage);
440
448
  const mappedSliderCommands = Command.mapMessages(sliderCommands, innerMessage => GotScrubberSliderMessage({ message: innerMessage }));
441
- const additionalCommands = Option.match(maybeOutMessage, {
442
- onNone: () => [],
449
+ // NOTE: the thumb tracks every pointermove (cheap, model-only via
450
+ // `nextSlider`), but the heavy jump-plus-inspect is coalesced to one
451
+ // navigation per animation frame. Each `ChangedValue` overwrites the
452
+ // pending host index; the `TickedScrubFrame` subscription flushes the
453
+ // latest on the next frame, so a fast drag can't enqueue jumps faster
454
+ // than they complete.
455
+ const nextMaybePendingScrubIndex = Option.match(maybeOutMessage, {
456
+ onNone: () => model.maybePendingScrubIndex,
443
457
  onSome: outMessage => M.value(outMessage).pipe(M.tagsExhaustive({
444
- ChangedValue: ({ value }) => {
445
- const hostIndex = sliderValueToHostIndex(value, model.startIndex);
446
- return [jumpTo(hostIndex), inspectState(hostIndex)];
447
- },
458
+ ChangedValue: ({ value }) => Option.some(sliderValueToHostIndex(value, model.startIndex)),
448
459
  })),
449
460
  });
450
461
  return [
451
- evo(model, { scrubberSlider: () => nextSlider }),
452
- [...mappedSliderCommands, ...additionalCommands],
462
+ evo(model, {
463
+ scrubberSlider: () => nextSlider,
464
+ maybePendingScrubIndex: () => nextMaybePendingScrubIndex,
465
+ }),
466
+ mappedSliderCommands,
453
467
  ];
454
468
  },
455
- }), M.tag('CompletedJump', 'CompletedResume', 'CompletedClear', 'LockedScroll', 'UnlockedScroll', 'ScrolledToTop', () => [model, []]), M.exhaustive);
469
+ TickedScrubFrame: () => Option.match(model.maybePendingScrubIndex, {
470
+ onNone: () => [model, []],
471
+ onSome: (hostIndex) => [
472
+ evo(model, { maybePendingScrubIndex: () => Option.none() }),
473
+ [jumpToAndInspect(hostIndex)],
474
+ ],
475
+ }),
476
+ }), M.tag('CompletedResume', 'CompletedClear', 'LockedScroll', 'UnlockedScroll', 'ScrolledToTop', () => [model, []]), M.exhaustive);
456
477
  };
457
478
  // SUBSCRIPTION
458
479
  const makeOverlaySubscriptions = (store, shadow) => {
@@ -465,6 +486,10 @@ const makeOverlaySubscriptions = (store, shadow) => {
465
486
  toParentMessage: message => GotScrubberSliderMessage({ message }),
466
487
  });
467
488
  const ownSubscriptions = Subscription.make()(_entry => ({
489
+ scrubFrame: Subscription.animationFrame({
490
+ isActive: model => Option.isSome(model.maybePendingScrubIndex),
491
+ toMessage: () => TickedScrubFrame(),
492
+ }),
468
493
  storeUpdates: Subscription.persistent(Stream.concat(Stream.fromEffect(SubscriptionRef.get(store.stateRef).pipe(Effect.map(state => ReceivedStoreUpdate(toDisplayState(state))))), Stream.map(SubscriptionRef.changes(store.stateRef), state => ReceivedStoreUpdate(toDisplayState(state))))),
469
494
  mobileBreakpoint: Subscription.persistent(Stream.callback(queue => Effect.acquireRelease(Effect.sync(() => {
470
495
  const mediaQuery = window.matchMedia(MOBILE_BREAKPOINT_QUERY);
@@ -660,7 +685,7 @@ const makeView = (position, mode, shadow, maybeBanner) => {
660
685
  const emptyInspectorView = () => h.div([
661
686
  h.Class('flex-1 flex items-center justify-center text-dt-muted text-2xs font-mono min-w-0'),
662
687
  ], ['Click a message to inspect']);
663
- const noMessageView = h.div([
688
+ const noMessageView = () => h.div([
664
689
  h.Class('flex-1 flex items-center justify-center text-dt-muted text-2xs font-mono min-w-0'),
665
690
  ], ['init: no Message']);
666
691
  const modelTabContent = (inspectedModel, expandedPaths, changedPaths, affectedPaths) => treeView(inspectedModel, 'root', expandedPaths, changedPaths, affectedPaths, Option.none(), true);
@@ -688,7 +713,7 @@ const makeView = (position, mode, shadow, maybeBanner) => {
688
713
  return current;
689
714
  };
690
715
  const messageTabContent = (maybeInspectedMessage, maybeSubmodelFilter, expandedPaths, timestamp) => Option.match(maybeInspectedMessage, {
691
- onNone: () => noMessageView,
716
+ onNone: noMessageView,
692
717
  onSome: rawMessage => {
693
718
  const message = unwrapIfFiltered(rawMessage, maybeSubmodelFilter);
694
719
  return h.div([h.Class('flex flex-col flex-1 min-h-0 min-w-0')], [
@@ -898,7 +923,7 @@ const makeView = (position, mode, shadow, maybeBanner) => {
898
923
  const headerClass = 'flex items-center justify-between px-3 py-1.5 border-b shrink-0';
899
924
  const actionButtonClass = 'dt-resume-button bg-transparent border-none text-dt-live cursor-pointer text-base font-mono font-medium';
900
925
  const statusClass = 'text-base font-mono';
901
- const clearHistoryButton = h.button([h.Class(headerButtonClass), h.OnClick(ClickedClear())], ['Clear history']);
926
+ const clearHistoryButton = () => h.button([h.Class(headerButtonClass), h.OnClick(ClickedClear())], ['Clear history']);
902
927
  const submodelLabel = (tag) => pipe(tag, String_.replace(/^Got/, ''), String_.replace(/Message$/, ''));
903
928
  const CHECK_ICON = 'M4.5 12.75l6 6 9-13.5';
904
929
  const checkIconView = () => h.svg([
@@ -929,7 +954,7 @@ const makeView = (position, mode, shadow, maybeBanner) => {
929
954
  ], [
930
955
  h.path([h.D(ARROW_UP), h.StrokeLinecap('round'), h.StrokeLinejoin('round')], []),
931
956
  ]);
932
- const scrollToTopPillView = h.button([
957
+ const scrollToTopPillView = () => h.button([
933
958
  h.Key('scroll-pill'),
934
959
  h.Class('dt-scroll-pill'),
935
960
  h.OnClick(ClickedScrollToTopPill()),
@@ -1000,7 +1025,7 @@ const makeView = (position, mode, shadow, maybeBanner) => {
1000
1025
  ]),
1001
1026
  maybeAction: OptionExt.when(!model.isFollowingLatest, h.button([h.Class(actionButtonClass), h.OnClick(ClickedFollowLatest())], ['Follow Latest →'])),
1002
1027
  })), M.exhaustive);
1003
- const maybeClearHistoryButton = OptionExt.when(!model.isPaused, clearHistoryButton);
1028
+ const maybeClearHistoryButton = OptionExt.when(!model.isPaused, clearHistoryButton());
1004
1029
  return h.header([h.Class(headerClass)], [
1005
1030
  status,
1006
1031
  ...Option.toArray(maybeAction),
@@ -1144,16 +1169,16 @@ const makeView = (position, mode, shadow, maybeBanner) => {
1144
1169
  onEmpty: () => [],
1145
1170
  onNonEmpty: () => [submodelFilterView(model)],
1146
1171
  }),
1147
- ...OptionExt.when(!model.isFollowingTop, scrollToTopPillView).pipe(Option.toArray),
1172
+ ...OptionExt.when(!model.isFollowingTop, scrollToTopPillView()).pipe(Option.toArray),
1148
1173
  messageListView(model),
1149
1174
  ]),
1150
1175
  inspectorPaneView(model),
1151
1176
  ]),
1152
1177
  ...OptionExt.when(isScrubberVisible, scrubberView(model)).pipe(Option.toArray),
1153
1178
  ]);
1154
- const interactionBlocker = h.div([h.Class('dt-interaction-blocker')], []);
1179
+ const interactionBlocker = () => h.div([h.Class('dt-interaction-blocker')], []);
1155
1180
  return (model) => h.div([], [
1156
- ...OptionExt.when(model.isPaused && mode === 'TimeTravel', interactionBlocker).pipe(Option.toArray),
1181
+ ...OptionExt.when(model.isPaused && mode === 'TimeTravel', interactionBlocker()).pipe(Option.toArray),
1157
1182
  ...OptionExt.when(model.isOpen, panelView(model)).pipe(Option.toArray),
1158
1183
  badgeView(model),
1159
1184
  ]);
@@ -1218,6 +1243,7 @@ export const createOverlay = (store, position, mode, maybeBanner) => Effect.gen(
1218
1243
  expandedPaths: HashSet.empty(),
1219
1244
  changedPaths: HashSet.empty(),
1220
1245
  affectedPaths: HashSet.empty(),
1246
+ maybePendingScrubIndex: Option.none(),
1221
1247
  inspectorTabs: Tabs.init({ id: INSPECTOR_TABS_ID }),
1222
1248
  scrubberSlider: Slider.init({
1223
1249
  id: SCRUBBER_SLIDER_ID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foldkit/devtools",
3
- "version": "0.113.1",
3
+ "version": "0.114.0",
4
4
  "description": "In-browser DevTools overlay for Foldkit applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -28,8 +28,8 @@
28
28
  "effect": "4.0.0-beta.83",
29
29
  "rimraf": "^6.1.3",
30
30
  "typescript": "^6.0.3",
31
- "@foldkit/ui": "0.113.1",
32
- "foldkit": "0.113.1"
31
+ "@foldkit/ui": "0.114.0",
32
+ "foldkit": "0.114.0"
33
33
  },
34
34
  "keywords": [
35
35
  "foldkit",