@pie-players/pie-section-player-tools-session-debugger 0.3.3 → 0.3.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.
@@ -12,6 +12,14 @@
12
12
 
13
13
  <script lang="ts">
14
14
  import '@pie-players/pie-theme/components.css';
15
+ import PanelResizeHandle from '@pie-players/pie-section-player-tools-shared/PanelResizeHandle.svelte';
16
+ import PanelWindowControls from '@pie-players/pie-section-player-tools-shared/PanelWindowControls.svelte';
17
+ import {
18
+ computePanelSizeFromViewport,
19
+ createFloatingPanelPointerController,
20
+ getSectionControllerFromCoordinator,
21
+ isMatchingSectionControllerLifecycleEvent
22
+ } from '@pie-players/pie-section-player-tools-shared';
15
23
  import { createEventDispatcher } from 'svelte';
16
24
  import { onMount } from 'svelte';
17
25
  const dispatch = createEventDispatcher<{ close: undefined }>();
@@ -21,6 +29,12 @@
21
29
  currentItemIndex: number | null;
22
30
  currentItemId: string | null;
23
31
  visitedItemIdentifiers: string[];
32
+ loadingComplete: boolean;
33
+ totalRegistered: number;
34
+ totalLoaded: number;
35
+ itemsComplete: boolean;
36
+ completedCount: number;
37
+ totalItems: number;
24
38
  updatedAt: number | null;
25
39
  lastChangedItemId: string | null;
26
40
  itemSessions: Record<string, unknown>;
@@ -31,15 +45,31 @@
31
45
  currentItemId?: string;
32
46
  visitedItemIdentifiers?: string[];
33
47
  itemSessions?: Record<string, unknown>;
48
+ loadingComplete?: boolean;
49
+ totalRegistered?: number;
50
+ totalLoaded?: number;
51
+ itemsComplete?: boolean;
52
+ completedCount?: number;
53
+ totalItems?: number;
54
+ };
55
+
56
+ type SectionSessionStateLike = {
57
+ itemSessions?: Record<string, unknown>;
34
58
  };
35
59
 
36
60
  type SectionControllerLike = {
37
- getCurrentSectionAttemptSlice?: () => SectionAttemptSliceLike | null;
61
+ getRuntimeState?: () => SectionAttemptSliceLike | null;
62
+ getSessionState?: () => SectionSessionStateLike | null;
38
63
  subscribe?: (listener: (event: { itemId?: string; timestamp?: number }) => void) => () => void;
39
64
  };
40
65
 
41
66
  type ToolkitCoordinatorLike = {
42
67
  getSectionController?: (args: { sectionId: string; attemptId?: string }) => SectionControllerLike | undefined;
68
+ subscribeSectionEvents: (args: {
69
+ sectionId: string;
70
+ attemptId?: string;
71
+ listener: (event: { itemId?: string; timestamp?: number }) => void;
72
+ }) => () => void;
43
73
  onSectionControllerLifecycle?: (
44
74
  listener: (event: { type: 'ready' | 'disposed'; key?: { sectionId?: string; attemptId?: string } }) => void
45
75
  ) => () => void;
@@ -60,31 +90,34 @@
60
90
  let sessionWindowY = $state(100);
61
91
  let sessionWindowWidth = $state(220);
62
92
  let sessionWindowHeight = $state(600);
63
- let isSessionDragging = $state(false);
64
- let isSessionResizing = $state(false);
65
-
66
- let dragStartX = 0;
67
- let dragStartY = 0;
68
- let dragStartWindowX = 0;
69
- let dragStartWindowY = 0;
70
- let resizeStartX = 0;
71
- let resizeStartY = 0;
72
- let resizeStartWidth = 0;
73
- let resizeStartHeight = 0;
74
93
 
75
94
  let sessionPanelSnapshot = $state<SessionPanelSnapshot>({
76
95
  currentItemIndex: null,
77
96
  currentItemId: null,
78
97
  visitedItemIdentifiers: [],
98
+ loadingComplete: false,
99
+ totalRegistered: 0,
100
+ totalLoaded: 0,
101
+ itemsComplete: false,
102
+ completedCount: 0,
103
+ totalItems: 0,
79
104
  updatedAt: null,
80
105
  lastChangedItemId: null,
81
106
  itemSessions: {}
82
107
  });
83
- let activeController: SectionControllerLike | null = null;
84
108
  let unsubscribeController: (() => void) | null = null;
85
109
  let unsubscribeLifecycle: (() => void) | null = null;
86
110
  let controllerAvailable = $state(false);
87
- let refreshQueued = false;
111
+ let resubscribeQueued = false;
112
+ const subscriptionTarget: {
113
+ controller: SectionControllerLike | null;
114
+ sectionId: string;
115
+ attemptId?: string;
116
+ } = {
117
+ controller: null,
118
+ sectionId: '',
119
+ attemptId: undefined
120
+ };
88
121
 
89
122
  function cloneSessionSnapshot<T>(value: T): T {
90
123
  try {
@@ -100,8 +133,13 @@
100
133
  }
101
134
 
102
135
  function getController(): SectionControllerLike | undefined {
103
- if (!toolkitCoordinator || !sectionId) return undefined;
104
- return toolkitCoordinator.getSectionController?.({ sectionId, attemptId });
136
+ return (
137
+ getSectionControllerFromCoordinator(
138
+ toolkitCoordinator,
139
+ sectionId,
140
+ attemptId
141
+ ) || undefined
142
+ );
105
143
  }
106
144
 
107
145
  function refreshFromController(
@@ -109,7 +147,8 @@
109
147
  controllerOverride?: SectionControllerLike | null
110
148
  ) {
111
149
  const controller = controllerOverride || getController();
112
- const sectionSlice = controller?.getCurrentSectionAttemptSlice?.() || null;
150
+ const sectionSlice = controller?.getRuntimeState?.() || null;
151
+ const persistedSlice = controller?.getSessionState?.() || null;
113
152
  controllerAvailable = Boolean(controller);
114
153
  sessionPanelSnapshot = {
115
154
  currentItemIndex:
@@ -121,30 +160,26 @@
121
160
  ? sectionSlice.currentItemId
122
161
  : null,
123
162
  visitedItemIdentifiers: cloneSessionSnapshot(sectionSlice?.visitedItemIdentifiers || []),
163
+ loadingComplete: sectionSlice?.loadingComplete === true,
164
+ totalRegistered: typeof sectionSlice?.totalRegistered === 'number' ? sectionSlice.totalRegistered : 0,
165
+ totalLoaded: typeof sectionSlice?.totalLoaded === 'number' ? sectionSlice.totalLoaded : 0,
166
+ itemsComplete: sectionSlice?.itemsComplete === true,
167
+ completedCount: typeof sectionSlice?.completedCount === 'number' ? sectionSlice.completedCount : 0,
168
+ totalItems: typeof sectionSlice?.totalItems === 'number' ? sectionSlice.totalItems : 0,
124
169
  updatedAt: meta?.updatedAt || Date.now(),
125
170
  lastChangedItemId: meta?.itemId || null,
126
- itemSessions: cloneSessionSnapshot(sectionSlice?.itemSessions || {})
171
+ itemSessions: cloneSessionSnapshot(
172
+ sectionSlice?.itemSessions || persistedSlice?.itemSessions || {}
173
+ )
127
174
  };
128
175
  }
129
176
 
130
- function queueRefresh(meta?: { itemId?: string; updatedAt?: number }) {
131
- if (refreshQueued) return;
132
- refreshQueued = true;
133
- queueMicrotask(() => {
134
- refreshQueued = false;
135
- ensureControllerSubscription();
136
- refreshFromController(
137
- meta || {
138
- updatedAt: Date.now()
139
- }
140
- );
141
- });
142
- }
143
-
144
177
  function detachControllerSubscription() {
145
178
  unsubscribeController?.();
146
179
  unsubscribeController = null;
147
- activeController = null;
180
+ subscriptionTarget.controller = null;
181
+ subscriptionTarget.sectionId = '';
182
+ subscriptionTarget.attemptId = undefined;
148
183
  }
149
184
 
150
185
  function detachLifecycleSubscription() {
@@ -152,6 +187,13 @@
152
187
  unsubscribeLifecycle = null;
153
188
  }
154
189
 
190
+ function handleControllerEvent(detail: { itemId?: string; timestamp?: number }): void {
191
+ refreshFromController({
192
+ itemId: detail?.itemId,
193
+ updatedAt: detail?.timestamp || Date.now()
194
+ });
195
+ }
196
+
155
197
  function ensureControllerSubscription() {
156
198
  const controller = getController() || null;
157
199
  if (!controller) {
@@ -161,32 +203,45 @@
161
203
  currentItemIndex: null,
162
204
  currentItemId: null,
163
205
  visitedItemIdentifiers: [],
206
+ loadingComplete: false,
207
+ totalRegistered: 0,
208
+ totalLoaded: 0,
209
+ itemsComplete: false,
210
+ completedCount: 0,
211
+ totalItems: 0,
164
212
  updatedAt: Date.now(),
165
213
  lastChangedItemId: null,
166
214
  itemSessions: {}
167
215
  };
168
216
  return;
169
217
  }
170
- if (controller === activeController) return;
218
+ const nextAttemptId = attemptId || undefined;
219
+ const isSameTarget =
220
+ subscriptionTarget.controller === controller &&
221
+ subscriptionTarget.sectionId === sectionId &&
222
+ subscriptionTarget.attemptId === nextAttemptId;
223
+ if (isSameTarget && unsubscribeController) {
224
+ refreshFromController(undefined, controller);
225
+ return;
226
+ }
171
227
  detachControllerSubscription();
172
- activeController = controller;
173
- const subscribe = typeof controller.subscribe === 'function' ? controller.subscribe.bind(controller) : null;
174
- unsubscribeController =
175
- subscribe?.((detail) => {
176
- refreshFromController(
177
- {
178
- itemId: detail?.itemId,
179
- updatedAt: detail?.timestamp || Date.now()
180
- },
181
- controller
182
- );
183
- }) || null;
228
+ unsubscribeController = toolkitCoordinator?.subscribeSectionEvents({
229
+ sectionId,
230
+ attemptId,
231
+ listener: handleControllerEvent
232
+ }) || null;
233
+ subscriptionTarget.controller = controller;
234
+ subscriptionTarget.sectionId = sectionId;
235
+ subscriptionTarget.attemptId = nextAttemptId;
184
236
  refreshFromController(undefined, controller);
185
237
  }
186
238
 
187
- export function refreshFromHost(): void {
188
- queueRefresh({
189
- updatedAt: Date.now()
239
+ function queueEnsureControllerSubscription(): void {
240
+ if (resubscribeQueued) return;
241
+ resubscribeQueued = true;
242
+ queueMicrotask(() => {
243
+ resubscribeQueued = false;
244
+ ensureControllerSubscription();
190
245
  });
191
246
  }
192
247
 
@@ -195,11 +250,8 @@
195
250
  ensureControllerSubscription();
196
251
  detachLifecycleSubscription();
197
252
  unsubscribeLifecycle = toolkitCoordinator.onSectionControllerLifecycle?.((event) => {
198
- const eventSectionId = event?.key?.sectionId || '';
199
- const eventAttemptId = event?.key?.attemptId || undefined;
200
- if (eventSectionId !== sectionId) return;
201
- if ((eventAttemptId || undefined) !== (attemptId || undefined)) return;
202
- ensureControllerSubscription();
253
+ if (!isMatchingSectionControllerLifecycleEvent(event, sectionId, attemptId)) return;
254
+ queueEnsureControllerSubscription();
203
255
  refreshFromController({
204
256
  updatedAt: Date.now()
205
257
  });
@@ -211,88 +263,47 @@
211
263
  });
212
264
 
213
265
  onMount(() => {
214
- const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(value, max));
215
- const viewportWidth = window.innerWidth;
216
- const viewportHeight = window.innerHeight;
217
- sessionWindowWidth = clamp(Math.round(viewportWidth * 0.29), 280, 560);
218
- sessionWindowHeight = clamp(Math.round(viewportHeight * 0.72), 360, 860);
219
- sessionWindowX = Math.max(16, Math.round(viewportWidth * 0.08));
220
- sessionWindowY = Math.max(16, Math.round((viewportHeight - sessionWindowHeight) / 2));
221
-
222
- const handleRuntimeSessionEvent = () => {
223
- queueRefresh({
224
- updatedAt: Date.now()
225
- });
226
- };
227
- document.addEventListener('session-changed', handleRuntimeSessionEvent as EventListener, true);
228
- document.addEventListener('item-session-changed', handleRuntimeSessionEvent as EventListener, true);
229
- document.addEventListener('composition-changed', handleRuntimeSessionEvent as EventListener, true);
230
- return () => {
231
- document.removeEventListener('session-changed', handleRuntimeSessionEvent as EventListener, true);
232
- document.removeEventListener('item-session-changed', handleRuntimeSessionEvent as EventListener, true);
233
- document.removeEventListener('composition-changed', handleRuntimeSessionEvent as EventListener, true);
234
- };
235
- });
236
-
237
- function startSessionDrag(e: MouseEvent) {
238
- isSessionDragging = true;
239
- dragStartX = e.clientX;
240
- dragStartY = e.clientY;
241
- dragStartWindowX = sessionWindowX;
242
- dragStartWindowY = sessionWindowY;
243
-
244
- document.addEventListener('mousemove', onSessionDrag);
245
- document.addEventListener('mouseup', stopSessionDrag);
246
- }
247
-
248
- function onSessionDrag(e: MouseEvent) {
249
- if (!isSessionDragging) return;
250
- const deltaX = e.clientX - dragStartX;
251
- const deltaY = e.clientY - dragStartY;
252
- sessionWindowX = dragStartWindowX + deltaX;
253
- sessionWindowY = dragStartWindowY + deltaY;
254
- sessionWindowX = Math.max(0, Math.min(sessionWindowX, window.innerWidth - sessionWindowWidth));
255
- sessionWindowY = Math.max(0, Math.min(sessionWindowY, window.innerHeight - 100));
256
- }
257
-
258
- function stopSessionDrag() {
259
- isSessionDragging = false;
260
- document.removeEventListener('mousemove', onSessionDrag);
261
- document.removeEventListener('mouseup', stopSessionDrag);
262
- }
263
-
264
- function startSessionResize(e: MouseEvent) {
265
- isSessionResizing = true;
266
- resizeStartX = e.clientX;
267
- resizeStartY = e.clientY;
268
- resizeStartWidth = sessionWindowWidth;
269
- resizeStartHeight = sessionWindowHeight;
270
- document.addEventListener('mousemove', onSessionResize);
271
- document.addEventListener('mouseup', stopSessionResize);
272
- e.stopPropagation();
273
- }
274
-
275
- function onSessionResize(e: MouseEvent) {
276
- if (!isSessionResizing) return;
277
- const deltaX = e.clientX - resizeStartX;
278
- const deltaY = e.clientY - resizeStartY;
279
- sessionWindowWidth = Math.max(300, Math.min(resizeStartWidth + deltaX, window.innerWidth - sessionWindowX));
280
- sessionWindowHeight = Math.max(
281
- 200,
282
- Math.min(resizeStartHeight + deltaY, window.innerHeight - sessionWindowY)
266
+ const initial = computePanelSizeFromViewport(
267
+ { width: window.innerWidth, height: window.innerHeight },
268
+ {
269
+ widthRatio: 0.29,
270
+ heightRatio: 0.72,
271
+ minWidth: 280,
272
+ maxWidth: 560,
273
+ minHeight: 360,
274
+ maxHeight: 860,
275
+ alignX: 'left',
276
+ alignY: 'center',
277
+ paddingX: 16,
278
+ paddingY: 16
279
+ }
283
280
  );
284
- }
281
+ sessionWindowX = initial.x;
282
+ sessionWindowY = initial.y;
283
+ sessionWindowWidth = initial.width;
284
+ sessionWindowHeight = initial.height;
285
+ });
285
286
 
286
- function stopSessionResize() {
287
- isSessionResizing = false;
288
- document.removeEventListener('mousemove', onSessionResize);
289
- document.removeEventListener('mouseup', stopSessionResize);
290
- }
287
+ const pointerController = createFloatingPanelPointerController({
288
+ getState: () => ({
289
+ x: sessionWindowX,
290
+ y: sessionWindowY,
291
+ width: sessionWindowWidth,
292
+ height: sessionWindowHeight
293
+ }),
294
+ setState: (next) => {
295
+ sessionWindowX = next.x;
296
+ sessionWindowY = next.y;
297
+ sessionWindowWidth = next.width;
298
+ sessionWindowHeight = next.height;
299
+ },
300
+ minWidth: 300,
301
+ minHeight: 200
302
+ });
291
303
 
292
304
  $effect(() => {
293
305
  return () => {
294
- stopSessionDrag();
295
- stopSessionResize();
306
+ pointerController.stop();
296
307
  };
297
308
  });
298
309
  </script>
@@ -303,7 +314,7 @@
303
314
  >
304
315
  <div
305
316
  class="pie-section-player-tools-session-debugger__header"
306
- onmousedown={startSessionDrag}
317
+ onmousedown={(event: MouseEvent) => pointerController.startDrag(event)}
307
318
  role="button"
308
319
  tabindex="0"
309
320
  aria-label="Drag session panel"
@@ -321,49 +332,16 @@
321
332
  <h3 class="pie-section-player-tools-session-debugger__title">Session Data</h3>
322
333
  </div>
323
334
  <div class="pie-section-player-tools-session-debugger__header-actions">
324
- <button
325
- class="pie-section-player-tools-session-debugger__icon-button"
326
- onclick={() => (isSessionMinimized = !isSessionMinimized)}
327
- title={isSessionMinimized ? 'Maximize' : 'Minimize'}
328
- >
329
- {#if isSessionMinimized}
330
- <svg
331
- xmlns="http://www.w3.org/2000/svg"
332
- class="pie-section-player-tools-session-debugger__icon-xs"
333
- fill="none"
334
- viewBox="0 0 24 24"
335
- stroke="currentColor"
336
- >
337
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
338
- </svg>
339
- {:else}
340
- <svg
341
- xmlns="http://www.w3.org/2000/svg"
342
- class="pie-section-player-tools-session-debugger__icon-xs"
343
- fill="none"
344
- viewBox="0 0 24 24"
345
- stroke="currentColor"
346
- >
347
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
348
- </svg>
349
- {/if}
350
- </button>
351
- <button class="pie-section-player-tools-session-debugger__icon-button" onclick={() => dispatch('close')} title="Close">
352
- <svg
353
- xmlns="http://www.w3.org/2000/svg"
354
- class="pie-section-player-tools-session-debugger__icon-xs"
355
- fill="none"
356
- viewBox="0 0 24 24"
357
- stroke="currentColor"
358
- >
359
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
360
- </svg>
361
- </button>
335
+ <PanelWindowControls
336
+ minimized={isSessionMinimized}
337
+ onToggle={() => (isSessionMinimized = !isSessionMinimized)}
338
+ onClose={() => dispatch('close')}
339
+ />
362
340
  </div>
363
341
  </div>
364
342
 
365
343
  {#if !isSessionMinimized}
366
- <div class="pie-section-player-tools-session-debugger__content-shell" style="height: {sessionWindowHeight - 60}px;">
344
+ <div class="pie-section-player-tools-session-debugger__content-shell" style="height: {sessionWindowHeight - 50}px;">
367
345
  <div class="pie-section-player-tools-session-debugger__content">
368
346
  <div class="pie-section-player-tools-session-debugger__section-intro">
369
347
  <div class="pie-section-player-tools-session-debugger__heading">PIE Session Data (Persistent)</div>
@@ -381,19 +359,20 @@
381
359
  </svg>
382
360
  <span class="pie-section-player-tools-session-debugger__text-xs">Section controller not available for this section yet.</span>
383
361
  </div>
384
- {:else if Object.keys(sessionPanelSnapshot.itemSessions || {}).length === 0}
385
- <div class="pie-section-player-tools-session-debugger__alert pie-section-player-tools-session-debugger__alert--info">
386
- <svg
387
- xmlns="http://www.w3.org/2000/svg"
388
- class="pie-section-player-tools-session-debugger__icon-md"
389
- fill="none"
390
- viewBox="0 0 24 24"
391
- >
392
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
393
- </svg>
394
- <span class="pie-section-player-tools-session-debugger__text-xs">No section session data yet. Interact with the questions to see updates.</span>
395
- </div>
396
362
  {:else}
363
+ {#if Object.keys(sessionPanelSnapshot.itemSessions || {}).length === 0}
364
+ <div class="pie-section-player-tools-session-debugger__alert pie-section-player-tools-session-debugger__alert--info">
365
+ <svg
366
+ xmlns="http://www.w3.org/2000/svg"
367
+ class="pie-section-player-tools-session-debugger__icon-md"
368
+ fill="none"
369
+ viewBox="0 0 24 24"
370
+ >
371
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
372
+ </svg>
373
+ <span class="pie-section-player-tools-session-debugger__text-xs">No section session data yet. Interact with the questions to see updates.</span>
374
+ </div>
375
+ {/if}
397
376
  <div class="pie-section-player-tools-session-debugger__card">
398
377
  <div class="pie-section-player-tools-session-debugger__card-title">
399
378
  Item Sessions Snapshot
@@ -406,18 +385,60 @@
406
385
  {/if}
407
386
 
408
387
  {#if !isSessionMinimized}
409
- <div
410
- class="pie-section-player-tools-session-debugger__resize-handle"
411
- onmousedown={startSessionResize}
412
- role="button"
413
- tabindex="0"
414
- title="Resize window"
415
- >
416
- <svg class="pie-section-player-tools-session-debugger__resize-icon" viewBox="0 0 16 16" fill="currentColor">
417
- <path d="M16 16V14H14V16H16Z" />
418
- <path d="M16 11V9H14V11H16Z" />
419
- <path d="M13 16V14H11V16H13Z" />
420
- </svg>
421
- </div>
388
+ <PanelResizeHandle onPointerDown={(event: MouseEvent) => pointerController.startResize(event)} />
422
389
  {/if}
423
390
  </div>
391
+
392
+ <style>
393
+ .pie-section-player-tools-session-debugger {
394
+ position: fixed;
395
+ z-index: 9999;
396
+ background: var(--color-base-100, #fff);
397
+ color: var(--color-base-content, #1f2937);
398
+ border: 2px solid var(--color-base-300, #d1d5db);
399
+ border-radius: 8px;
400
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
401
+ overflow: hidden;
402
+ font-family: var(--pie-font-family, Inter, system-ui, sans-serif);
403
+ }
404
+
405
+ .pie-section-player-tools-session-debugger__header {
406
+ padding: 8px 16px;
407
+ display: flex;
408
+ align-items: center;
409
+ justify-content: space-between;
410
+ background: var(--color-base-200, #f3f4f6);
411
+ cursor: move;
412
+ user-select: none;
413
+ border-bottom: 1px solid var(--color-base-300, #d1d5db);
414
+ }
415
+
416
+ .pie-section-player-tools-session-debugger__header-title {
417
+ display: flex;
418
+ align-items: center;
419
+ gap: 8px;
420
+ }
421
+
422
+ .pie-section-player-tools-session-debugger__icon-sm {
423
+ width: 1rem;
424
+ height: 1rem;
425
+ }
426
+
427
+ .pie-section-player-tools-session-debugger__title {
428
+ margin: 0;
429
+ font-size: 0.95rem;
430
+ font-weight: 700;
431
+ }
432
+
433
+ .pie-section-player-tools-session-debugger__header-actions {
434
+ display: flex;
435
+ gap: 4px;
436
+ }
437
+
438
+ .pie-section-player-tools-session-debugger__content-shell {
439
+ display: flex;
440
+ flex-direction: column;
441
+ min-height: 0;
442
+ }
443
+
444
+ </style>