@ngstato/angular 0.4.0 → 0.4.2
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/README.md +161 -394
- package/dist/devtools.component.d.ts +15 -1
- package/dist/esm2022/create-angular-store.mjs +45 -9
- package/dist/esm2022/devtools.component.mjs +255 -45
- package/dist/fesm2022/ngstato-angular.mjs +297 -51
- package/dist/fesm2022/ngstato-angular.mjs.map +1 -1
- package/package.json +62 -63
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { inject, InjectionToken, isDevMode, makeEnvironmentProviders, signal, computed, Injectable, Component } from '@angular/core';
|
|
3
3
|
import { configureHttp, devTools, createStore } from '@ngstato/core';
|
|
4
|
-
import { JsonPipe } from '@angular/common';
|
|
4
|
+
import { JsonPipe, KeyValuePipe } from '@angular/common';
|
|
5
5
|
|
|
6
6
|
// ─────────────────────────────────────────────────────
|
|
7
7
|
// @ngstato/angular — injectStore()
|
|
@@ -39,8 +39,10 @@ function provideStato(config = {}) {
|
|
|
39
39
|
// FONCTION PRINCIPALE — createAngularStore()
|
|
40
40
|
// ─────────────────────────────────────────────────────
|
|
41
41
|
function createAngularStore(config) {
|
|
42
|
-
// 1. Créer le store core
|
|
43
|
-
|
|
42
|
+
// 1. Créer le store core — SANS l'initialiser
|
|
43
|
+
// L'init sera fait après que l'angularStore soit prêt,
|
|
44
|
+
// pour que les effects reçoivent la bonne référence dès le 1er run.
|
|
45
|
+
const coreStore = createStore(config, { skipInit: true });
|
|
44
46
|
// 2. Créer un Signal pour chaque propriété du state
|
|
45
47
|
const signals = {};
|
|
46
48
|
const initialState = coreStore.getState();
|
|
@@ -68,10 +70,22 @@ function createAngularStore(config) {
|
|
|
68
70
|
});
|
|
69
71
|
}
|
|
70
72
|
// 6. Exposer chaque computed comme Signal computed
|
|
71
|
-
|
|
73
|
+
// On force la lecture des signals Angular pour que le computed()
|
|
74
|
+
// soit réactif via le système de change detection d'Angular.
|
|
75
|
+
const { computed: computedConfig, selectors: selectorsConfig } = config;
|
|
72
76
|
if (computedConfig) {
|
|
73
77
|
for (const key of Object.keys(computedConfig)) {
|
|
74
|
-
const
|
|
78
|
+
const fn = computedConfig[key];
|
|
79
|
+
if (typeof fn !== 'function')
|
|
80
|
+
continue;
|
|
81
|
+
const computedSignal = computed(() => {
|
|
82
|
+
// Lire les signals Angular pour déclencher le tracking
|
|
83
|
+
const snapshot = {};
|
|
84
|
+
for (const stateKey of Object.keys(signals)) {
|
|
85
|
+
snapshot[stateKey] = signals[stateKey]();
|
|
86
|
+
}
|
|
87
|
+
return fn(snapshot);
|
|
88
|
+
});
|
|
75
89
|
Object.defineProperty(angularStore, key, {
|
|
76
90
|
get: () => computedSignal,
|
|
77
91
|
enumerable: true,
|
|
@@ -79,6 +93,26 @@ function createAngularStore(config) {
|
|
|
79
93
|
});
|
|
80
94
|
}
|
|
81
95
|
}
|
|
96
|
+
// 6b. Exposer chaque selector memoïzé comme Signal computed
|
|
97
|
+
if (selectorsConfig) {
|
|
98
|
+
for (const key of Object.keys(selectorsConfig)) {
|
|
99
|
+
const fn = selectorsConfig[key];
|
|
100
|
+
if (typeof fn !== 'function')
|
|
101
|
+
continue;
|
|
102
|
+
const selectorSignal = computed(() => {
|
|
103
|
+
const snapshot = {};
|
|
104
|
+
for (const stateKey of Object.keys(signals)) {
|
|
105
|
+
snapshot[stateKey] = signals[stateKey]();
|
|
106
|
+
}
|
|
107
|
+
return fn(snapshot);
|
|
108
|
+
});
|
|
109
|
+
Object.defineProperty(angularStore, key, {
|
|
110
|
+
get: () => selectorSignal,
|
|
111
|
+
enumerable: true,
|
|
112
|
+
configurable: true
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
82
116
|
// 7. Exposer chaque action directement
|
|
83
117
|
const { actions } = config;
|
|
84
118
|
if (actions) {
|
|
@@ -86,13 +120,15 @@ function createAngularStore(config) {
|
|
|
86
120
|
angularStore[name] = (...args) => coreStore.__store__.dispatch(name, ...args);
|
|
87
121
|
}
|
|
88
122
|
}
|
|
89
|
-
// 8.
|
|
90
|
-
// (init est idempotent: onInit ne sera appelé qu'une fois)
|
|
91
|
-
coreStore.__store__.init(angularStore);
|
|
92
|
-
// 9. Exposer destroy pour le cleanup
|
|
123
|
+
// 8. Exposer destroy pour le cleanup
|
|
93
124
|
angularStore.__destroy__ = () => {
|
|
94
125
|
coreStore.__store__.destroy(angularStore);
|
|
95
126
|
};
|
|
127
|
+
// 9. Initialiser le store UNE SEULE FOIS avec l'angularStore
|
|
128
|
+
// C'est ici que onInit() est appelé et que les effects démarrent,
|
|
129
|
+
// directement avec la bonne référence (angularStore avec Signals).
|
|
130
|
+
// Aucun double _runEffects — un seul init, un seul run.
|
|
131
|
+
coreStore.__store__.init(angularStore);
|
|
96
132
|
return angularStore;
|
|
97
133
|
}
|
|
98
134
|
// ─────────────────────────────────────────────────────
|
|
@@ -245,11 +281,36 @@ class StatoDevToolsComponent {
|
|
|
245
281
|
activeTab = signal('actions');
|
|
246
282
|
logs = signal([]);
|
|
247
283
|
selectedLog = signal(null);
|
|
248
|
-
|
|
284
|
+
activeLogId = signal(null);
|
|
285
|
+
isTimeTraveling = signal(false);
|
|
286
|
+
// Snapshot global — latest known state per store
|
|
287
|
+
globalState = computed(() => {
|
|
288
|
+
const seen = new Map();
|
|
289
|
+
for (const log of this.logs()) {
|
|
290
|
+
if (!seen.has(log.storeName)) {
|
|
291
|
+
seen.set(log.storeName, log.nextState);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return seen;
|
|
295
|
+
});
|
|
296
|
+
// Time-travel computed
|
|
297
|
+
canUndo = computed(() => this.logs().length > 0);
|
|
298
|
+
canRedo = computed(() => {
|
|
299
|
+
if (!this.isTimeTraveling())
|
|
300
|
+
return false;
|
|
301
|
+
const id = this.activeLogId();
|
|
302
|
+
if (id === null)
|
|
303
|
+
return false;
|
|
304
|
+
if (id === -1)
|
|
305
|
+
return this.logs().length > 0;
|
|
306
|
+
const idx = this.logs().findIndex(l => l.id === id);
|
|
307
|
+
return idx > 0; // can go forward (lower index = newer)
|
|
308
|
+
});
|
|
309
|
+
// Position & size
|
|
249
310
|
posX = signal(24);
|
|
250
|
-
posY = signal(window.innerHeight -
|
|
251
|
-
panelWidth = signal(
|
|
252
|
-
panelHeight = signal(
|
|
311
|
+
posY = signal(window.innerHeight - 520);
|
|
312
|
+
panelWidth = signal(440);
|
|
313
|
+
panelHeight = signal(480);
|
|
253
314
|
// Drag state
|
|
254
315
|
isDragging = false;
|
|
255
316
|
isResizing = false;
|
|
@@ -266,6 +327,8 @@ class StatoDevToolsComponent {
|
|
|
266
327
|
this.unsub = devTools.subscribe((state) => {
|
|
267
328
|
this.logs.set(state.logs);
|
|
268
329
|
this.isOpen.set(state.isOpen);
|
|
330
|
+
this.activeLogId.set(state.activeLogId);
|
|
331
|
+
this.isTimeTraveling.set(state.isTimeTraveling);
|
|
269
332
|
});
|
|
270
333
|
document.addEventListener('mousemove', this.boundMouseMove);
|
|
271
334
|
document.addEventListener('mouseup', this.boundMouseUp);
|
|
@@ -285,6 +348,65 @@ class StatoDevToolsComponent {
|
|
|
285
348
|
formatTime(iso) {
|
|
286
349
|
return new Date(iso).toTimeString().slice(0, 8);
|
|
287
350
|
}
|
|
351
|
+
// ── Time-travel actions ────────────────────────────
|
|
352
|
+
onTravelTo(log) {
|
|
353
|
+
// Toggle detail view
|
|
354
|
+
this.selectLog(log);
|
|
355
|
+
// Jump to this action's state
|
|
356
|
+
devTools.travelTo(log.id);
|
|
357
|
+
}
|
|
358
|
+
onUndo() { devTools.undo(); }
|
|
359
|
+
onRedo() { devTools.redo(); }
|
|
360
|
+
onResume() { devTools.resume(); }
|
|
361
|
+
onReplay(log, event) {
|
|
362
|
+
event.stopPropagation();
|
|
363
|
+
devTools.replay(log.id);
|
|
364
|
+
}
|
|
365
|
+
isFutureLog(log) {
|
|
366
|
+
if (!this.isTimeTraveling())
|
|
367
|
+
return false;
|
|
368
|
+
const activeId = this.activeLogId();
|
|
369
|
+
if (activeId === null)
|
|
370
|
+
return false;
|
|
371
|
+
if (activeId === -1)
|
|
372
|
+
return true; // all logs are "future"
|
|
373
|
+
const activeIdx = this.logs().findIndex(l => l.id === activeId);
|
|
374
|
+
const logIdx = this.logs().findIndex(l => l.id === log.id);
|
|
375
|
+
return logIdx < activeIdx; // newer logs (lower index) are "future"
|
|
376
|
+
}
|
|
377
|
+
// ── Export/Import ──────────────────────────────────
|
|
378
|
+
onExport() {
|
|
379
|
+
const snapshot = devTools.exportSnapshot();
|
|
380
|
+
const json = JSON.stringify(snapshot, null, 2);
|
|
381
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
382
|
+
const url = URL.createObjectURL(blob);
|
|
383
|
+
const a = document.createElement('a');
|
|
384
|
+
a.href = url;
|
|
385
|
+
a.download = `ngstato-snapshot-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
|
|
386
|
+
a.click();
|
|
387
|
+
URL.revokeObjectURL(url);
|
|
388
|
+
}
|
|
389
|
+
onImport() {
|
|
390
|
+
const input = document.querySelector('input[type="file"]');
|
|
391
|
+
input?.click();
|
|
392
|
+
}
|
|
393
|
+
onFileSelected(event) {
|
|
394
|
+
const file = event.target.files?.[0];
|
|
395
|
+
if (!file)
|
|
396
|
+
return;
|
|
397
|
+
const reader = new FileReader();
|
|
398
|
+
reader.onload = () => {
|
|
399
|
+
try {
|
|
400
|
+
const snapshot = JSON.parse(reader.result);
|
|
401
|
+
devTools.importSnapshot(snapshot);
|
|
402
|
+
}
|
|
403
|
+
catch (e) {
|
|
404
|
+
console.error('[ngStato DevTools] Invalid snapshot file:', e);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
reader.readAsText(file);
|
|
408
|
+
event.target.value = '';
|
|
409
|
+
}
|
|
288
410
|
// ── Drag ───────────────────────────────────────────
|
|
289
411
|
onDragStart(e) {
|
|
290
412
|
if (e.target.classList.contains('btn-icon'))
|
|
@@ -323,8 +445,8 @@ class StatoDevToolsComponent {
|
|
|
323
445
|
this.isResizing = false;
|
|
324
446
|
}
|
|
325
447
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: StatoDevToolsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
326
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: StatoDevToolsComponent, isStandalone: true, selector: "ngstato-devtools", ngImport: i0, template: `
|
|
327
|
-
<!--
|
|
448
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: StatoDevToolsComponent, isStandalone: true, selector: "ngstato-devtools, stato-devtools", ngImport: i0, template: `
|
|
449
|
+
<!-- Floating button -->
|
|
328
450
|
@if (!isOpen()) {
|
|
329
451
|
<button class="devtools-fab" (click)="toggle()">
|
|
330
452
|
🛠 Stato
|
|
@@ -338,7 +460,7 @@ class StatoDevToolsComponent {
|
|
|
338
460
|
[class.devtools-panel--minimized]="isMinimized()"
|
|
339
461
|
[style.left.px]="posX()"
|
|
340
462
|
[style.top.px]="posY()"
|
|
341
|
-
[style.width.px]="isMinimized() ?
|
|
463
|
+
[style.width.px]="isMinimized() ? 220 : panelWidth()"
|
|
342
464
|
[style.height]="isMinimized() ? 'auto' : panelHeight() + 'px'"
|
|
343
465
|
>
|
|
344
466
|
|
|
@@ -347,19 +469,24 @@ class StatoDevToolsComponent {
|
|
|
347
469
|
class="devtools-header"
|
|
348
470
|
(mousedown)="onDragStart($event)"
|
|
349
471
|
>
|
|
350
|
-
<span class="devtools-title"
|
|
472
|
+
<span class="devtools-title">
|
|
473
|
+
🛠 Stato
|
|
474
|
+
@if (isTimeTraveling()) {
|
|
475
|
+
<span class="tt-badge">TIME-TRAVEL</span>
|
|
476
|
+
}
|
|
477
|
+
</span>
|
|
351
478
|
<div class="devtools-header-actions">
|
|
352
479
|
@if (!isMinimized()) {
|
|
353
|
-
<button class="btn-icon" (click)="clear()" title="
|
|
480
|
+
<button class="btn-icon" (click)="clear()" title="Clear">🗑</button>
|
|
354
481
|
}
|
|
355
|
-
<button class="btn-icon" (click)="toggleMinimize()" title="
|
|
482
|
+
<button class="btn-icon" (click)="toggleMinimize()" title="Minimize">
|
|
356
483
|
{{ isMinimized() ? '▲' : '▼' }}
|
|
357
484
|
</button>
|
|
358
|
-
<button class="btn-icon" (click)="toggle()" title="
|
|
485
|
+
<button class="btn-icon" (click)="toggle()" title="Close">✕</button>
|
|
359
486
|
</div>
|
|
360
487
|
</div>
|
|
361
488
|
|
|
362
|
-
<!-- Resize handle
|
|
489
|
+
<!-- Resize handle -->
|
|
363
490
|
@if (!isMinimized()) {
|
|
364
491
|
<div
|
|
365
492
|
class="devtools-resize"
|
|
@@ -387,17 +514,55 @@ class StatoDevToolsComponent {
|
|
|
387
514
|
</button>
|
|
388
515
|
</div>
|
|
389
516
|
|
|
390
|
-
<!--
|
|
517
|
+
<!-- Time-travel toolbar -->
|
|
518
|
+
@if (activeTab() === 'actions' && logs().length) {
|
|
519
|
+
<div class="tt-toolbar">
|
|
520
|
+
<button
|
|
521
|
+
class="tt-btn"
|
|
522
|
+
(click)="onUndo()"
|
|
523
|
+
[disabled]="!canUndo()"
|
|
524
|
+
title="Undo (step back)"
|
|
525
|
+
>⏪</button>
|
|
526
|
+
<button
|
|
527
|
+
class="tt-btn"
|
|
528
|
+
(click)="onRedo()"
|
|
529
|
+
[disabled]="!canRedo()"
|
|
530
|
+
title="Redo (step forward)"
|
|
531
|
+
>⏩</button>
|
|
532
|
+
@if (isTimeTraveling()) {
|
|
533
|
+
<button
|
|
534
|
+
class="tt-btn tt-btn--resume"
|
|
535
|
+
(click)="onResume()"
|
|
536
|
+
title="Resume live state"
|
|
537
|
+
>▶ Live</button>
|
|
538
|
+
}
|
|
539
|
+
<div class="tt-spacer"></div>
|
|
540
|
+
<button
|
|
541
|
+
class="tt-btn tt-btn--export"
|
|
542
|
+
(click)="onExport()"
|
|
543
|
+
title="Export state snapshot (JSON)"
|
|
544
|
+
>📤</button>
|
|
545
|
+
<button
|
|
546
|
+
class="tt-btn tt-btn--import"
|
|
547
|
+
(click)="onImport()"
|
|
548
|
+
title="Import state snapshot"
|
|
549
|
+
>📥</button>
|
|
550
|
+
</div>
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
<!-- Tab: Actions -->
|
|
391
554
|
@if (activeTab() === 'actions') {
|
|
392
555
|
<div class="devtools-content">
|
|
393
556
|
@if (!logs().length) {
|
|
394
|
-
<div class="devtools-empty">
|
|
557
|
+
<div class="devtools-empty">No actions yet</div>
|
|
395
558
|
}
|
|
396
559
|
@for (log of logs(); track log.id) {
|
|
397
560
|
<div
|
|
398
561
|
class="log-item"
|
|
399
562
|
[class.log-item--error]="log.status === 'error'"
|
|
400
|
-
|
|
563
|
+
[class.log-item--active]="activeLogId() === log.id"
|
|
564
|
+
[class.log-item--future]="isFutureLog(log)"
|
|
565
|
+
(click)="onTravelTo(log)"
|
|
401
566
|
>
|
|
402
567
|
<div class="log-item__left">
|
|
403
568
|
<span class="log-status">{{ log.status === 'success' ? '✓' : '✗' }}</span>
|
|
@@ -405,11 +570,16 @@ class StatoDevToolsComponent {
|
|
|
405
570
|
</div>
|
|
406
571
|
<div class="log-item__right">
|
|
407
572
|
@if (log.status === 'error') {
|
|
408
|
-
<span class="log-error-badge">
|
|
573
|
+
<span class="log-error-badge">error</span>
|
|
409
574
|
} @else {
|
|
410
575
|
<span class="log-duration">{{ log.duration }}ms</span>
|
|
411
576
|
}
|
|
412
577
|
<span class="log-time">{{ formatTime(log.at) }}</span>
|
|
578
|
+
<button
|
|
579
|
+
class="btn-icon btn-replay"
|
|
580
|
+
(click)="onReplay(log, $event)"
|
|
581
|
+
title="Replay this action"
|
|
582
|
+
>🔄</button>
|
|
413
583
|
</div>
|
|
414
584
|
</div>
|
|
415
585
|
|
|
@@ -419,11 +589,11 @@ class StatoDevToolsComponent {
|
|
|
419
589
|
<div class="log-detail__error">{{ log.error }}</div>
|
|
420
590
|
}
|
|
421
591
|
<div class="log-detail__section">
|
|
422
|
-
<span class="log-detail__label">
|
|
592
|
+
<span class="log-detail__label">Before</span>
|
|
423
593
|
<pre>{{ log.prevState | json }}</pre>
|
|
424
594
|
</div>
|
|
425
595
|
<div class="log-detail__section">
|
|
426
|
-
<span class="log-detail__label">
|
|
596
|
+
<span class="log-detail__label">After</span>
|
|
427
597
|
<pre>{{ log.nextState | json }}</pre>
|
|
428
598
|
</div>
|
|
429
599
|
</div>
|
|
@@ -432,13 +602,18 @@ class StatoDevToolsComponent {
|
|
|
432
602
|
</div>
|
|
433
603
|
}
|
|
434
604
|
|
|
435
|
-
<!-- Tab State -->
|
|
605
|
+
<!-- Tab: State -->
|
|
436
606
|
@if (activeTab() === 'state') {
|
|
437
607
|
<div class="devtools-content">
|
|
438
|
-
@if (
|
|
439
|
-
|
|
608
|
+
@if (globalState().size) {
|
|
609
|
+
@for (entry of globalState() | keyvalue; track entry.key) {
|
|
610
|
+
<div class="state-store-block">
|
|
611
|
+
<div class="state-store-name">{{ entry.key }}</div>
|
|
612
|
+
<pre class="state-view">{{ entry.value | json }}</pre>
|
|
613
|
+
</div>
|
|
614
|
+
}
|
|
440
615
|
} @else {
|
|
441
|
-
<div class="devtools-empty">
|
|
616
|
+
<div class="devtools-empty">No state available</div>
|
|
442
617
|
}
|
|
443
618
|
</div>
|
|
444
619
|
}
|
|
@@ -446,12 +621,21 @@ class StatoDevToolsComponent {
|
|
|
446
621
|
|
|
447
622
|
</div>
|
|
448
623
|
}
|
|
449
|
-
|
|
624
|
+
|
|
625
|
+
<!-- Hidden file input for import -->
|
|
626
|
+
<input
|
|
627
|
+
#fileInput
|
|
628
|
+
type="file"
|
|
629
|
+
accept=".json"
|
|
630
|
+
style="display: none"
|
|
631
|
+
(change)="onFileSelected($event)"
|
|
632
|
+
/>
|
|
633
|
+
`, isInline: true, styles: [":host{font-family:system-ui,-apple-system,sans-serif}.devtools-fab{position:fixed;bottom:1.5rem;left:1.5rem;background:#1e293b;color:#fff;border:none;border-radius:999px;padding:.5rem 1rem;font-size:.85rem;font-weight:600;cursor:pointer;z-index:9999;box-shadow:0 4px 12px #0000004d;transition:background .15s}.devtools-fab:hover{background:#334155}.devtools-panel{position:fixed;background:#0f172a;border-radius:12px;box-shadow:0 8px 32px #00000080;z-index:9999;display:flex;flex-direction:column;overflow:hidden;font-family:Courier New,monospace;min-width:200px;min-height:40px;border:1px solid #1e293b}.devtools-panel--minimized{border-radius:8px}.devtools-header{display:flex;justify-content:space-between;align-items:center;padding:.6rem .75rem;background:#1e293b;border-bottom:1px solid #334155;cursor:grab;-webkit-user-select:none;user-select:none}.devtools-header:active{cursor:grabbing}.devtools-title{color:#e2e8f0;font-size:.82rem;font-weight:600;font-family:system-ui;display:flex;align-items:center;gap:.4rem}.tt-badge{background:#7c3aed;color:#fff;font-size:.6rem;padding:.12rem .4rem;border-radius:4px;font-weight:700;letter-spacing:.05em;animation:tt-pulse 1.5s ease-in-out infinite}@keyframes tt-pulse{0%,to{opacity:1}50%{opacity:.6}}.devtools-header-actions{display:flex;gap:.25rem}.btn-icon{background:transparent;color:#64748b;border:none;cursor:pointer;font-size:.8rem;padding:.15rem .35rem;border-radius:4px;line-height:1}.btn-icon:hover{background:#334155;color:#fff}.devtools-resize{position:absolute;bottom:2px;right:4px;color:#334155;font-size:.9rem;cursor:nwse-resize;-webkit-user-select:none;user-select:none;line-height:1}.devtools-resize:hover{color:#64748b}.devtools-tabs{display:flex;background:#1e293b;border-bottom:1px solid #334155}.tab{padding:.4rem .75rem;background:transparent;color:#64748b;border:none;cursor:pointer;font-size:.78rem;font-family:system-ui;transition:color .15s}.tab:hover{color:#e2e8f0}.tab--active{color:#3b82f6;border-bottom:2px solid #3b82f6}.tt-toolbar{display:flex;align-items:center;gap:.2rem;padding:.3rem .5rem;background:#0f172a;border-bottom:1px solid #1e293b}.tt-btn{background:#1e293b;border:1px solid #334155;color:#94a3b8;padding:.2rem .5rem;border-radius:4px;font-size:.72rem;cursor:pointer;transition:all .15s;font-family:system-ui}.tt-btn:hover:not(:disabled){background:#334155;color:#e2e8f0;border-color:#475569}.tt-btn:disabled{opacity:.3;cursor:not-allowed}.tt-btn--resume{background:#7c3aed;border-color:#7c3aed;color:#fff;font-weight:600}.tt-btn--resume:hover:not(:disabled){background:#6d28d9}.tt-btn--export,.tt-btn--import{font-size:.68rem}.tt-spacer{flex:1}.devtools-content{overflow-y:auto;flex:1;padding:.25rem 0}.devtools-empty{padding:2rem;text-align:center;color:#475569;font-size:.78rem;font-family:system-ui}.log-item{display:flex;justify-content:space-between;align-items:center;padding:.35rem .75rem;cursor:pointer;border-bottom:1px solid #1e293b;transition:background .1s}.log-item:hover{background:#1e293b}.log-item--error{background:#1a0a0a}.log-item--active{background:#1e1b4b!important;border-left:3px solid #7c3aed}.log-item--future{opacity:.35}.log-item__left{display:flex;align-items:center;gap:.4rem;overflow:hidden;flex:1}.log-item__right{display:flex;align-items:center;gap:.35rem;flex-shrink:0}.log-status{font-size:.72rem;color:#22c55e;flex-shrink:0}.log-item--error .log-status{color:#ef4444}.log-name{color:#e2e8f0;font-size:.75rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.log-duration{color:#64748b;font-size:.7rem}.log-time{color:#475569;font-size:.68rem}.log-error-badge{background:#7f1d1d;color:#fca5a5;font-size:.68rem;padding:.1rem .35rem;border-radius:4px}.btn-replay{font-size:.65rem;opacity:.5}.btn-replay:hover{opacity:1}.log-detail{background:#0a0f1a;padding:.6rem .75rem;border-left:3px solid #3b82f6;margin:0 .4rem .4rem;border-radius:0 4px 4px 0}.log-detail__error{color:#fca5a5;font-size:.72rem;margin-bottom:.4rem}.log-detail__section{margin-bottom:.4rem}.log-detail__label{color:#64748b;font-size:.68rem;display:block;margin-bottom:.2rem;font-family:system-ui}pre{color:#86efac;font-size:.7rem;margin:0;white-space:pre-wrap;word-break:break-all;max-height:140px;overflow-y:auto}.state-store-block{border-bottom:1px solid #1e293b}.state-store-block:last-child{border-bottom:none}.state-store-name{padding:.4rem .75rem .2rem;color:#3b82f6;font-size:.72rem;font-family:system-ui;font-weight:600;letter-spacing:.03em;text-transform:uppercase}.state-view{color:#86efac;font-size:.7rem;padding:.25rem .75rem .75rem;margin:0;white-space:pre-wrap;word-break:break-all}\n"], dependencies: [{ kind: "pipe", type: JsonPipe, name: "json" }, { kind: "pipe", type: KeyValuePipe, name: "keyvalue" }] });
|
|
450
634
|
}
|
|
451
635
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: StatoDevToolsComponent, decorators: [{
|
|
452
636
|
type: Component,
|
|
453
|
-
args: [{ selector: 'ngstato-devtools', standalone: true, imports: [JsonPipe], template: `
|
|
454
|
-
<!--
|
|
637
|
+
args: [{ selector: 'ngstato-devtools, stato-devtools', standalone: true, imports: [JsonPipe, KeyValuePipe], template: `
|
|
638
|
+
<!-- Floating button -->
|
|
455
639
|
@if (!isOpen()) {
|
|
456
640
|
<button class="devtools-fab" (click)="toggle()">
|
|
457
641
|
🛠 Stato
|
|
@@ -465,7 +649,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
465
649
|
[class.devtools-panel--minimized]="isMinimized()"
|
|
466
650
|
[style.left.px]="posX()"
|
|
467
651
|
[style.top.px]="posY()"
|
|
468
|
-
[style.width.px]="isMinimized() ?
|
|
652
|
+
[style.width.px]="isMinimized() ? 220 : panelWidth()"
|
|
469
653
|
[style.height]="isMinimized() ? 'auto' : panelHeight() + 'px'"
|
|
470
654
|
>
|
|
471
655
|
|
|
@@ -474,19 +658,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
474
658
|
class="devtools-header"
|
|
475
659
|
(mousedown)="onDragStart($event)"
|
|
476
660
|
>
|
|
477
|
-
<span class="devtools-title"
|
|
661
|
+
<span class="devtools-title">
|
|
662
|
+
🛠 Stato
|
|
663
|
+
@if (isTimeTraveling()) {
|
|
664
|
+
<span class="tt-badge">TIME-TRAVEL</span>
|
|
665
|
+
}
|
|
666
|
+
</span>
|
|
478
667
|
<div class="devtools-header-actions">
|
|
479
668
|
@if (!isMinimized()) {
|
|
480
|
-
<button class="btn-icon" (click)="clear()" title="
|
|
669
|
+
<button class="btn-icon" (click)="clear()" title="Clear">🗑</button>
|
|
481
670
|
}
|
|
482
|
-
<button class="btn-icon" (click)="toggleMinimize()" title="
|
|
671
|
+
<button class="btn-icon" (click)="toggleMinimize()" title="Minimize">
|
|
483
672
|
{{ isMinimized() ? '▲' : '▼' }}
|
|
484
673
|
</button>
|
|
485
|
-
<button class="btn-icon" (click)="toggle()" title="
|
|
674
|
+
<button class="btn-icon" (click)="toggle()" title="Close">✕</button>
|
|
486
675
|
</div>
|
|
487
676
|
</div>
|
|
488
677
|
|
|
489
|
-
<!-- Resize handle
|
|
678
|
+
<!-- Resize handle -->
|
|
490
679
|
@if (!isMinimized()) {
|
|
491
680
|
<div
|
|
492
681
|
class="devtools-resize"
|
|
@@ -514,17 +703,55 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
514
703
|
</button>
|
|
515
704
|
</div>
|
|
516
705
|
|
|
517
|
-
<!--
|
|
706
|
+
<!-- Time-travel toolbar -->
|
|
707
|
+
@if (activeTab() === 'actions' && logs().length) {
|
|
708
|
+
<div class="tt-toolbar">
|
|
709
|
+
<button
|
|
710
|
+
class="tt-btn"
|
|
711
|
+
(click)="onUndo()"
|
|
712
|
+
[disabled]="!canUndo()"
|
|
713
|
+
title="Undo (step back)"
|
|
714
|
+
>⏪</button>
|
|
715
|
+
<button
|
|
716
|
+
class="tt-btn"
|
|
717
|
+
(click)="onRedo()"
|
|
718
|
+
[disabled]="!canRedo()"
|
|
719
|
+
title="Redo (step forward)"
|
|
720
|
+
>⏩</button>
|
|
721
|
+
@if (isTimeTraveling()) {
|
|
722
|
+
<button
|
|
723
|
+
class="tt-btn tt-btn--resume"
|
|
724
|
+
(click)="onResume()"
|
|
725
|
+
title="Resume live state"
|
|
726
|
+
>▶ Live</button>
|
|
727
|
+
}
|
|
728
|
+
<div class="tt-spacer"></div>
|
|
729
|
+
<button
|
|
730
|
+
class="tt-btn tt-btn--export"
|
|
731
|
+
(click)="onExport()"
|
|
732
|
+
title="Export state snapshot (JSON)"
|
|
733
|
+
>📤</button>
|
|
734
|
+
<button
|
|
735
|
+
class="tt-btn tt-btn--import"
|
|
736
|
+
(click)="onImport()"
|
|
737
|
+
title="Import state snapshot"
|
|
738
|
+
>📥</button>
|
|
739
|
+
</div>
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
<!-- Tab: Actions -->
|
|
518
743
|
@if (activeTab() === 'actions') {
|
|
519
744
|
<div class="devtools-content">
|
|
520
745
|
@if (!logs().length) {
|
|
521
|
-
<div class="devtools-empty">
|
|
746
|
+
<div class="devtools-empty">No actions yet</div>
|
|
522
747
|
}
|
|
523
748
|
@for (log of logs(); track log.id) {
|
|
524
749
|
<div
|
|
525
750
|
class="log-item"
|
|
526
751
|
[class.log-item--error]="log.status === 'error'"
|
|
527
|
-
|
|
752
|
+
[class.log-item--active]="activeLogId() === log.id"
|
|
753
|
+
[class.log-item--future]="isFutureLog(log)"
|
|
754
|
+
(click)="onTravelTo(log)"
|
|
528
755
|
>
|
|
529
756
|
<div class="log-item__left">
|
|
530
757
|
<span class="log-status">{{ log.status === 'success' ? '✓' : '✗' }}</span>
|
|
@@ -532,11 +759,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
532
759
|
</div>
|
|
533
760
|
<div class="log-item__right">
|
|
534
761
|
@if (log.status === 'error') {
|
|
535
|
-
<span class="log-error-badge">
|
|
762
|
+
<span class="log-error-badge">error</span>
|
|
536
763
|
} @else {
|
|
537
764
|
<span class="log-duration">{{ log.duration }}ms</span>
|
|
538
765
|
}
|
|
539
766
|
<span class="log-time">{{ formatTime(log.at) }}</span>
|
|
767
|
+
<button
|
|
768
|
+
class="btn-icon btn-replay"
|
|
769
|
+
(click)="onReplay(log, $event)"
|
|
770
|
+
title="Replay this action"
|
|
771
|
+
>🔄</button>
|
|
540
772
|
</div>
|
|
541
773
|
</div>
|
|
542
774
|
|
|
@@ -546,11 +778,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
546
778
|
<div class="log-detail__error">{{ log.error }}</div>
|
|
547
779
|
}
|
|
548
780
|
<div class="log-detail__section">
|
|
549
|
-
<span class="log-detail__label">
|
|
781
|
+
<span class="log-detail__label">Before</span>
|
|
550
782
|
<pre>{{ log.prevState | json }}</pre>
|
|
551
783
|
</div>
|
|
552
784
|
<div class="log-detail__section">
|
|
553
|
-
<span class="log-detail__label">
|
|
785
|
+
<span class="log-detail__label">After</span>
|
|
554
786
|
<pre>{{ log.nextState | json }}</pre>
|
|
555
787
|
</div>
|
|
556
788
|
</div>
|
|
@@ -559,13 +791,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
559
791
|
</div>
|
|
560
792
|
}
|
|
561
793
|
|
|
562
|
-
<!-- Tab State -->
|
|
794
|
+
<!-- Tab: State -->
|
|
563
795
|
@if (activeTab() === 'state') {
|
|
564
796
|
<div class="devtools-content">
|
|
565
|
-
@if (
|
|
566
|
-
|
|
797
|
+
@if (globalState().size) {
|
|
798
|
+
@for (entry of globalState() | keyvalue; track entry.key) {
|
|
799
|
+
<div class="state-store-block">
|
|
800
|
+
<div class="state-store-name">{{ entry.key }}</div>
|
|
801
|
+
<pre class="state-view">{{ entry.value | json }}</pre>
|
|
802
|
+
</div>
|
|
803
|
+
}
|
|
567
804
|
} @else {
|
|
568
|
-
<div class="devtools-empty">
|
|
805
|
+
<div class="devtools-empty">No state available</div>
|
|
569
806
|
}
|
|
570
807
|
</div>
|
|
571
808
|
}
|
|
@@ -573,7 +810,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
573
810
|
|
|
574
811
|
</div>
|
|
575
812
|
}
|
|
576
|
-
|
|
813
|
+
|
|
814
|
+
<!-- Hidden file input for import -->
|
|
815
|
+
<input
|
|
816
|
+
#fileInput
|
|
817
|
+
type="file"
|
|
818
|
+
accept=".json"
|
|
819
|
+
style="display: none"
|
|
820
|
+
(change)="onFileSelected($event)"
|
|
821
|
+
/>
|
|
822
|
+
`, styles: [":host{font-family:system-ui,-apple-system,sans-serif}.devtools-fab{position:fixed;bottom:1.5rem;left:1.5rem;background:#1e293b;color:#fff;border:none;border-radius:999px;padding:.5rem 1rem;font-size:.85rem;font-weight:600;cursor:pointer;z-index:9999;box-shadow:0 4px 12px #0000004d;transition:background .15s}.devtools-fab:hover{background:#334155}.devtools-panel{position:fixed;background:#0f172a;border-radius:12px;box-shadow:0 8px 32px #00000080;z-index:9999;display:flex;flex-direction:column;overflow:hidden;font-family:Courier New,monospace;min-width:200px;min-height:40px;border:1px solid #1e293b}.devtools-panel--minimized{border-radius:8px}.devtools-header{display:flex;justify-content:space-between;align-items:center;padding:.6rem .75rem;background:#1e293b;border-bottom:1px solid #334155;cursor:grab;-webkit-user-select:none;user-select:none}.devtools-header:active{cursor:grabbing}.devtools-title{color:#e2e8f0;font-size:.82rem;font-weight:600;font-family:system-ui;display:flex;align-items:center;gap:.4rem}.tt-badge{background:#7c3aed;color:#fff;font-size:.6rem;padding:.12rem .4rem;border-radius:4px;font-weight:700;letter-spacing:.05em;animation:tt-pulse 1.5s ease-in-out infinite}@keyframes tt-pulse{0%,to{opacity:1}50%{opacity:.6}}.devtools-header-actions{display:flex;gap:.25rem}.btn-icon{background:transparent;color:#64748b;border:none;cursor:pointer;font-size:.8rem;padding:.15rem .35rem;border-radius:4px;line-height:1}.btn-icon:hover{background:#334155;color:#fff}.devtools-resize{position:absolute;bottom:2px;right:4px;color:#334155;font-size:.9rem;cursor:nwse-resize;-webkit-user-select:none;user-select:none;line-height:1}.devtools-resize:hover{color:#64748b}.devtools-tabs{display:flex;background:#1e293b;border-bottom:1px solid #334155}.tab{padding:.4rem .75rem;background:transparent;color:#64748b;border:none;cursor:pointer;font-size:.78rem;font-family:system-ui;transition:color .15s}.tab:hover{color:#e2e8f0}.tab--active{color:#3b82f6;border-bottom:2px solid #3b82f6}.tt-toolbar{display:flex;align-items:center;gap:.2rem;padding:.3rem .5rem;background:#0f172a;border-bottom:1px solid #1e293b}.tt-btn{background:#1e293b;border:1px solid #334155;color:#94a3b8;padding:.2rem .5rem;border-radius:4px;font-size:.72rem;cursor:pointer;transition:all .15s;font-family:system-ui}.tt-btn:hover:not(:disabled){background:#334155;color:#e2e8f0;border-color:#475569}.tt-btn:disabled{opacity:.3;cursor:not-allowed}.tt-btn--resume{background:#7c3aed;border-color:#7c3aed;color:#fff;font-weight:600}.tt-btn--resume:hover:not(:disabled){background:#6d28d9}.tt-btn--export,.tt-btn--import{font-size:.68rem}.tt-spacer{flex:1}.devtools-content{overflow-y:auto;flex:1;padding:.25rem 0}.devtools-empty{padding:2rem;text-align:center;color:#475569;font-size:.78rem;font-family:system-ui}.log-item{display:flex;justify-content:space-between;align-items:center;padding:.35rem .75rem;cursor:pointer;border-bottom:1px solid #1e293b;transition:background .1s}.log-item:hover{background:#1e293b}.log-item--error{background:#1a0a0a}.log-item--active{background:#1e1b4b!important;border-left:3px solid #7c3aed}.log-item--future{opacity:.35}.log-item__left{display:flex;align-items:center;gap:.4rem;overflow:hidden;flex:1}.log-item__right{display:flex;align-items:center;gap:.35rem;flex-shrink:0}.log-status{font-size:.72rem;color:#22c55e;flex-shrink:0}.log-item--error .log-status{color:#ef4444}.log-name{color:#e2e8f0;font-size:.75rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.log-duration{color:#64748b;font-size:.7rem}.log-time{color:#475569;font-size:.68rem}.log-error-badge{background:#7f1d1d;color:#fca5a5;font-size:.68rem;padding:.1rem .35rem;border-radius:4px}.btn-replay{font-size:.65rem;opacity:.5}.btn-replay:hover{opacity:1}.log-detail{background:#0a0f1a;padding:.6rem .75rem;border-left:3px solid #3b82f6;margin:0 .4rem .4rem;border-radius:0 4px 4px 0}.log-detail__error{color:#fca5a5;font-size:.72rem;margin-bottom:.4rem}.log-detail__section{margin-bottom:.4rem}.log-detail__label{color:#64748b;font-size:.68rem;display:block;margin-bottom:.2rem;font-family:system-ui}pre{color:#86efac;font-size:.7rem;margin:0;white-space:pre-wrap;word-break:break-all;max-height:140px;overflow-y:auto}.state-store-block{border-bottom:1px solid #1e293b}.state-store-block:last-child{border-bottom:none}.state-store-name{padding:.4rem .75rem .2rem;color:#3b82f6;font-size:.72rem;font-family:system-ui;font-weight:600;letter-spacing:.03em;text-transform:uppercase}.state-view{color:#86efac;font-size:.7rem;padding:.25rem .75rem .75rem;margin:0;white-space:pre-wrap;word-break:break-all}\n"] }]
|
|
577
823
|
}] });
|
|
578
824
|
|
|
579
825
|
// ─────────────────────────────────────────────────────
|