@stackwright-pro/pulse 0.2.0 → 0.2.1-alpha.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/index.mjs CHANGED
@@ -4,6 +4,8 @@ import { useState as useState2, useEffect as useEffect2 } from "react";
4
4
  // src/hooks/usePulse.ts
5
5
  import { useState, useCallback, useEffect } from "react";
6
6
  import { useQuery } from "@tanstack/react-query";
7
+ var MIN_POLL_INTERVAL = 2e3;
8
+ var MAX_POLL_INTERVAL = 3e5;
7
9
  function usePulse(options) {
8
10
  const {
9
11
  fetcher,
@@ -26,7 +28,7 @@ function usePulse(options) {
26
28
  const { data, isLoading, isFetching, isSuccess, isError, error, refetch } = useQuery({
27
29
  queryKey,
28
30
  queryFn: validatedFetcher,
29
- refetchInterval: enabled ? Math.max(interval, 1e3) : false,
31
+ refetchInterval: enabled ? Math.min(Math.max(interval ?? 5e3, MIN_POLL_INTERVAL), MAX_POLL_INTERVAL) : false,
30
32
  refetchOnWindowFocus,
31
33
  retry: retryCount,
32
34
  retryDelay: (attemptIndex) => Math.min(1e3 * 2 ** attemptIndex, 1e4),
@@ -88,16 +90,15 @@ function Pulse({
88
90
  emptyState,
89
91
  showStaleDataOnError = true
90
92
  }) {
91
- const { data, meta, state } = usePulse({
92
- fetcher,
93
- interval,
94
- staleThreshold,
95
- maxStaleAge,
96
- schema,
97
- enabled,
98
- refetchOnWindowFocus,
99
- retryCount
100
- });
93
+ const pulseOptions = { fetcher };
94
+ if (interval !== void 0) pulseOptions.interval = interval;
95
+ if (staleThreshold !== void 0) pulseOptions.staleThreshold = staleThreshold;
96
+ if (maxStaleAge !== void 0) pulseOptions.maxStaleAge = maxStaleAge;
97
+ if (schema !== void 0) pulseOptions.schema = schema;
98
+ if (enabled !== void 0) pulseOptions.enabled = enabled;
99
+ if (refetchOnWindowFocus !== void 0) pulseOptions.refetchOnWindowFocus = refetchOnWindowFocus;
100
+ if (retryCount !== void 0) pulseOptions.retryCount = retryCount;
101
+ const { data, meta, state } = usePulse(pulseOptions);
101
102
  if (state === "loading" && data === void 0) {
102
103
  return /* @__PURE__ */ jsx(Fragment, { children: loadingState ?? /* @__PURE__ */ jsx(DefaultLoading, {}) });
103
104
  }
@@ -314,8 +315,366 @@ var PulseValidationError = class extends Error {
314
315
  }));
315
316
  }
316
317
  };
318
+
319
+ // src/collection/PulseCollectionProvider.tsx
320
+ import React4, { createContext, useContext, useMemo } from "react";
321
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
322
+ var ALLOWED_COLLECTIONS = /* @__PURE__ */ new Set();
323
+ function setAllowedCollections(collections) {
324
+ ALLOWED_COLLECTIONS.clear();
325
+ collections.forEach((c) => ALLOWED_COLLECTIONS.add(c));
326
+ }
327
+ var PulseCollectionContext = createContext(null);
328
+ function resolveTemplate(template, collections) {
329
+ const match = template.match(/\{\{\s*([\w.]+)\s*\}\}/);
330
+ if (!match || match[1] === void 0) return template;
331
+ const path = match[1];
332
+ const parts = path.split(".");
333
+ const collection = parts[0];
334
+ if (!collection) return template;
335
+ if (!ALLOWED_COLLECTIONS.has(collection)) {
336
+ console.warn(`[PulseCollectionProvider] Collection "${collection}" not in allowed list`);
337
+ return template;
338
+ }
339
+ const data = collections[collection];
340
+ if (!data) return template;
341
+ let value = data;
342
+ for (let i = 1; i < parts.length; i++) {
343
+ const key = parts[i];
344
+ if (!key) return template;
345
+ if (value && typeof value === "object") {
346
+ value = value[key];
347
+ } else {
348
+ return template;
349
+ }
350
+ }
351
+ return value ?? template;
352
+ }
353
+ async function fetchCollectionData(collectionName) {
354
+ try {
355
+ const module = await import("./collectionData-3ZJA4PEZ.mjs");
356
+ return module.getCollectionData(collectionName);
357
+ } catch {
358
+ console.warn(`[PulseCollectionProvider] collectionData not generated for: ${collectionName}`);
359
+ return [];
360
+ }
361
+ }
362
+ function PulseCollectionProvider({
363
+ collections: collectionConfigs,
364
+ children,
365
+ fallback
366
+ }) {
367
+ React4.useEffect(() => {
368
+ setAllowedCollections(collectionConfigs.map((c) => c.collection));
369
+ }, [collectionConfigs]);
370
+ const pulseConfigs = useMemo(() => {
371
+ return collectionConfigs.map((config) => ({
372
+ name: config.collection,
373
+ fetcher: () => fetchCollectionData(config.collection),
374
+ interval: config.refreshInterval || 5e3
375
+ }));
376
+ }, [collectionConfigs]);
377
+ const [collectionsData, setCollectionsData] = React4.useState({});
378
+ const [allLoading, setAllLoading] = React4.useState(true);
379
+ const pulseElements = pulseConfigs.map((config) => /* @__PURE__ */ jsx4(
380
+ Pulse,
381
+ {
382
+ fetcher: config.fetcher,
383
+ interval: config.interval,
384
+ staleThreshold: 3e4,
385
+ maxStaleAge: 6e4,
386
+ children: (data, meta) => {
387
+ setCollectionsData((prev) => ({
388
+ ...prev,
389
+ [config.name]: {
390
+ items: Array.isArray(data) ? data : [],
391
+ count: Array.isArray(data) ? data.length : 1,
392
+ meta,
393
+ ...data
394
+ }
395
+ }));
396
+ if (pulseConfigs.length === Object.keys({ ...collectionsData, [config.name]: true }).length) {
397
+ setAllLoading(false);
398
+ }
399
+ return null;
400
+ }
401
+ },
402
+ config.name
403
+ ));
404
+ const contextValue = {
405
+ collections: collectionsData,
406
+ isLoading: allLoading,
407
+ getCollection: (name) => collectionsData[name] || null,
408
+ getField: (collection, field) => {
409
+ const data = collectionsData[collection];
410
+ if (!data) return void 0;
411
+ return data[field] ?? resolveTemplate(field, collectionsData);
412
+ }
413
+ };
414
+ if (allLoading) {
415
+ return /* @__PURE__ */ jsx4(Fragment2, { children: fallback ?? /* @__PURE__ */ jsx4(DefaultLoading2, {}) });
416
+ }
417
+ return /* @__PURE__ */ jsxs4(PulseCollectionContext.Provider, { value: contextValue, children: [
418
+ pulseElements,
419
+ children
420
+ ] });
421
+ }
422
+ function usePulseCollections() {
423
+ const context = useContext(PulseCollectionContext);
424
+ if (!context) {
425
+ throw new Error("usePulseCollections must be used within PulseCollectionProvider");
426
+ }
427
+ return context;
428
+ }
429
+ function useCollection(collectionName) {
430
+ const { getCollection } = usePulseCollections();
431
+ return getCollection(collectionName);
432
+ }
433
+ function useCollectionField(collectionName, field) {
434
+ const { getField } = usePulseCollections();
435
+ return getField(collectionName, field);
436
+ }
437
+ function useTemplateResolution() {
438
+ const { collections } = usePulseCollections();
439
+ return (template) => {
440
+ return resolveTemplate(template, collections);
441
+ };
442
+ }
443
+ function DefaultLoading2() {
444
+ return /* @__PURE__ */ jsx4("div", { style: { padding: "2rem", textAlign: "center", color: "#6B7280" }, children: "Loading live data..." });
445
+ }
446
+
447
+ // src/collection/MetricCardPulse.tsx
448
+ import React5 from "react";
449
+ import { z } from "zod";
450
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
451
+ var MetricCardPulseSchema = z.object({
452
+ collection: z.string().min(1, "Collection name required"),
453
+ field: z.string().min(1, "Field path required"),
454
+ label: z.string().optional(),
455
+ icon: z.any().optional(),
456
+ // ReactNode
457
+ color: z.string().optional(),
458
+ trend: z.enum(["up", "down", "stable"]).optional(),
459
+ trendValue: z.string().optional(),
460
+ aggregate: z.enum(["count", "sum", "avg"]).optional(),
461
+ aggregateField: z.string().optional(),
462
+ filter: z.string().optional()
463
+ });
464
+ function runInDev(fn) {
465
+ try {
466
+ if (process.env.NODE_ENV !== "production") {
467
+ fn();
468
+ }
469
+ } catch {
470
+ }
471
+ }
472
+ function MetricCard({ label, value, icon, color, trend, trendValue }) {
473
+ const cardColor = color ?? "#0066CC";
474
+ return /* @__PURE__ */ jsxs5(
475
+ "div",
476
+ {
477
+ style: {
478
+ backgroundColor: "white",
479
+ borderRadius: "12px",
480
+ padding: "24px",
481
+ boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
482
+ border: "1px solid #E5E7EB",
483
+ position: "relative",
484
+ overflow: "hidden"
485
+ },
486
+ children: [
487
+ /* @__PURE__ */ jsx5(
488
+ "div",
489
+ {
490
+ style: {
491
+ position: "absolute",
492
+ top: 0,
493
+ left: 0,
494
+ right: 0,
495
+ height: "4px",
496
+ backgroundColor: cardColor
497
+ }
498
+ }
499
+ ),
500
+ /* @__PURE__ */ jsxs5("div", { style: { display: "flex", alignItems: "flex-start", justifyContent: "space-between" }, children: [
501
+ /* @__PURE__ */ jsxs5("div", { style: { flex: 1 }, children: [
502
+ /* @__PURE__ */ jsx5(
503
+ "p",
504
+ {
505
+ style: {
506
+ fontSize: "14px",
507
+ color: "#6B7280",
508
+ margin: "0 0 8px 0",
509
+ fontWeight: 500
510
+ },
511
+ children: label
512
+ }
513
+ ),
514
+ /* @__PURE__ */ jsx5(
515
+ "p",
516
+ {
517
+ style: {
518
+ fontSize: "36px",
519
+ fontWeight: 700,
520
+ color: "#111827",
521
+ margin: 0,
522
+ lineHeight: 1
523
+ },
524
+ children: typeof value === "number" ? value.toLocaleString() : value
525
+ }
526
+ ),
527
+ trend && trendValue && /* @__PURE__ */ jsx5(
528
+ "div",
529
+ {
530
+ style: {
531
+ display: "flex",
532
+ alignItems: "center",
533
+ gap: "4px",
534
+ marginTop: "12px"
535
+ },
536
+ children: /* @__PURE__ */ jsxs5(
537
+ "span",
538
+ {
539
+ style: {
540
+ fontSize: "13px",
541
+ color: "#6B7280"
542
+ },
543
+ children: [
544
+ trend === "up" ? "\u2191" : trend === "down" ? "\u2193" : "\u2192",
545
+ " ",
546
+ trendValue
547
+ ]
548
+ }
549
+ )
550
+ }
551
+ )
552
+ ] }),
553
+ icon && /* @__PURE__ */ jsx5(
554
+ "div",
555
+ {
556
+ style: {
557
+ width: "48px",
558
+ height: "48px",
559
+ borderRadius: "12px",
560
+ backgroundColor: `${cardColor}15`,
561
+ display: "flex",
562
+ alignItems: "center",
563
+ justifyContent: "center",
564
+ color: cardColor
565
+ },
566
+ children: icon
567
+ }
568
+ )
569
+ ] })
570
+ ]
571
+ }
572
+ );
573
+ }
574
+ function MetricCardPulse({
575
+ collection,
576
+ field,
577
+ label,
578
+ icon,
579
+ color,
580
+ trend,
581
+ trendValue,
582
+ aggregate,
583
+ aggregateField
584
+ }) {
585
+ runInDev(() => {
586
+ const result = MetricCardPulseSchema.safeParse({
587
+ collection,
588
+ field,
589
+ label,
590
+ icon,
591
+ color,
592
+ trend,
593
+ trendValue,
594
+ aggregate,
595
+ aggregateField
596
+ });
597
+ if (!result.success) {
598
+ console.warn("[MetricCardPulse] Invalid props:", result.error.issues);
599
+ }
600
+ });
601
+ const value = useCollectionField(collection, field);
602
+ const displayValue = React5.useMemo(() => {
603
+ if (value === null || value === void 0) return 0;
604
+ if (aggregate === "count") {
605
+ return Array.isArray(value) ? value.length : value ?? 0;
606
+ }
607
+ if ((aggregate === "sum" || aggregate === "avg") && aggregateField) {
608
+ const arr = Array.isArray(value) ? value : [];
609
+ if (aggregate === "sum") {
610
+ return arr.reduce((sum2, item) => sum2 + (item[aggregateField] ?? 0), 0);
611
+ }
612
+ if (arr.length === 0) return 0;
613
+ const sum = arr.reduce((s, item) => s + (item[aggregateField] ?? 0), 0);
614
+ return Math.round(sum / arr.length * 10) / 10;
615
+ }
616
+ return typeof value === "number" ? value : 0;
617
+ }, [value, aggregate, aggregateField]);
618
+ return /* @__PURE__ */ jsx5(
619
+ MetricCard,
620
+ {
621
+ label: label ?? field,
622
+ value: typeof displayValue === "number" ? displayValue : 0,
623
+ icon,
624
+ color,
625
+ trend,
626
+ trendValue
627
+ }
628
+ );
629
+ }
630
+
631
+ // src/collection/DataTablePulse.tsx
632
+ import { useMemo as useMemo2 } from "react";
633
+ import { z as z2 } from "zod";
634
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
635
+ var ColumnSchema = z2.object({
636
+ field: z2.string().min(1),
637
+ header: z2.string(),
638
+ type: z2.enum(["text", "badge", "date", "number"]).optional(),
639
+ sortable: z2.boolean().optional(),
640
+ filterable: z2.boolean().optional()
641
+ });
642
+ var DataTablePulseSchema = z2.object({
643
+ collection: z2.string().min(1),
644
+ columns: z2.array(ColumnSchema).min(1, "At least one column required"),
645
+ filter: z2.string().optional(),
646
+ sortBy: z2.string().optional(),
647
+ sortDirection: z2.enum(["asc", "desc"]).optional(),
648
+ limit: z2.number().int().positive().max(1e3).optional(),
649
+ onRowClick: z2.function().optional(),
650
+ emptyMessage: z2.string().optional()
651
+ });
652
+
653
+ // src/collection/StatusBadgePulse.tsx
654
+ import React7 from "react";
655
+ import { z as z3 } from "zod";
656
+ import { StatusBadge } from "@stackwright-pro/display-components";
657
+ import { jsx as jsx7 } from "react/jsx-runtime";
658
+ var StatusBadgePulseSchema = z3.object({
659
+ collection: z3.string().min(1),
660
+ field: z3.string().min(1),
661
+ label: z3.string().optional(),
662
+ pulse: z3.boolean().optional(),
663
+ statusMap: z3.record(z3.string(), z3.enum(["operational", "degraded", "outage", "maintenance"])).optional()
664
+ });
665
+
666
+ // src/registration.ts
667
+ import { registerComponent } from "@stackwright/core";
668
+ function registerPulseComponents() {
669
+ registerComponent("pulse_provider", () => null);
670
+ registerComponent("metric_card_pulse", () => null);
671
+ registerComponent("data_table_pulse", () => null);
672
+ registerComponent("status_badge_pulse", () => null);
673
+ }
317
674
  export {
675
+ MetricCardPulse,
318
676
  Pulse,
677
+ PulseCollectionProvider,
319
678
  PulseEmptyState,
320
679
  PulseErrorState,
321
680
  PulseIndicator,
@@ -324,6 +683,12 @@ export {
324
683
  PulseSyncingState,
325
684
  PulseValidationError,
326
685
  createPulseValidator,
686
+ registerPulseComponents,
687
+ resolveTemplate,
688
+ useCollection,
689
+ useCollectionField,
327
690
  usePulse,
328
- useStreaming
691
+ usePulseCollections,
692
+ useStreaming,
693
+ useTemplateResolution
329
694
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackwright-pro/pulse",
3
- "version": "0.2.0",
3
+ "version": "0.2.1-alpha.0",
4
4
  "description": "Source-agnostic real-time data polling for Stackwright Pro",
5
5
  "license": "PROPRIETARY",
6
6
  "main": "./dist/index.js",
@@ -18,16 +18,22 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "@tanstack/react-query": "^5.0.0",
21
- "zod": "^4.3.6"
21
+ "zod": "^4.3.6",
22
+ "@stackwright-pro/display-components": "0.1.2-alpha.0"
22
23
  },
23
24
  "peerDependencies": {
24
- "react": "^18.0.0",
25
- "react-dom": "^18.0.0"
25
+ "@stackwright/core": ">=0.7.0",
26
+ "react": "^18.0.0 || ^19.0.0",
27
+ "react-dom": "^18.0.0 || ^19.0.0"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
26
31
  },
27
32
  "devDependencies": {
33
+ "@testing-library/react": "^16.0.0",
34
+ "@types/node": "^24.12.0",
28
35
  "@types/react": "^18.3.0",
29
36
  "@types/react-dom": "^18.3.0",
30
- "@testing-library/react": "^16.0.0",
31
37
  "jsdom": "^25.0.0",
32
38
  "tsup": "^8.5.0",
33
39
  "typescript": "^5.8.3",
@@ -36,6 +42,7 @@
36
42
  "scripts": {
37
43
  "build": "tsup src/index.ts --format cjs,esm --dts --clean",
38
44
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
39
- "test": "vitest"
45
+ "test": "vitest",
46
+ "test:coverage": "vitest run --coverage"
40
47
  }
41
48
  }