portal-mojo 0.2.13 → 0.2.14

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.
@@ -6,7 +6,7 @@
6
6
 
7
7
  export interface SeriesLike {
8
8
  label: string;
9
- data: number[];
9
+ data: (number | null | undefined)[];
10
10
  }
11
11
 
12
12
  export interface SeriesStat {
@@ -31,15 +31,17 @@ export function toNumber(v: unknown): number {
31
31
  /**
32
32
  * Per-series summary stats. Empty datasets produce no entry (the caller
33
33
  * renders its "No data" state); an empty series inside a non-empty set
34
- * yields a zeroed row so the table stays aligned with the legend.
34
+ * yields unavailable (NaN) values so the table stays aligned with the legend.
35
+ * Aggregates use finite observations only; Latest refers to the final sample.
35
36
  */
36
37
  export function computeSeriesStats(datasets: readonly SeriesLike[] | null | undefined): SeriesStat[] {
37
38
  // (Array.isArray would widen the readonly array to any[] — a TS quirk.)
38
39
  const list: readonly SeriesLike[] = datasets ?? [];
39
40
  return list.map((d) => {
40
- const values = (Array.isArray(d.data) ? d.data : []).map(toNumber);
41
+ const data = Array.isArray(d.data) ? d.data : [];
42
+ const values = data.filter((v): v is number => typeof v === 'number' && Number.isFinite(v));
41
43
  if (values.length === 0) {
42
- return { label: d.label ?? '', count: 0, sum: 0, avg: 0, median: 0, min: 0, max: 0, latest: 0 };
44
+ return { label: d.label ?? '', count: 0, sum: NaN, avg: NaN, median: NaN, min: NaN, max: NaN, latest: NaN };
43
45
  }
44
46
  const sorted = [...values].sort((a, b) => a - b);
45
47
  const sum = values.reduce((a, b) => a + b, 0);
@@ -53,14 +55,14 @@ export function computeSeriesStats(datasets: readonly SeriesLike[] | null | unde
53
55
  median,
54
56
  min: sorted[0]!,
55
57
  max: sorted[sorted.length - 1]!,
56
- latest: values[values.length - 1]!,
58
+ latest: Number.isFinite(data.at(-1)) ? data.at(-1)! : NaN,
57
59
  };
58
60
  });
59
61
  }
60
62
 
61
63
  /** '–' for nothing; integers plain; decimals to 2 places (source parity). */
62
64
  export function defaultStatFormat(n: number | null | undefined): string {
63
- if (n == null || Number.isNaN(n)) return '–';
65
+ if (n == null || !Number.isFinite(n)) return '–';
64
66
  if (Number.isInteger(n)) return n.toLocaleString();
65
67
  return Number(n).toLocaleString(undefined, { maximumFractionDigits: 2 });
66
68
  }
@@ -247,3 +247,18 @@ button.cprog { cursor: pointer; font: inherit; }
247
247
 
248
248
  /* Settings dialog fields */
249
249
  .mmw-settings { display: flex; flex-direction: column; gap: 12px; }
250
+
251
+ /* Shared MetricsChartView also lives in narrow cards inside wide pages. */
252
+ .chart-panel { container-type: inline-size; min-width: 0; }
253
+ .chart-panel .chart-error { flex-wrap: wrap; overflow-wrap: anywhere; }
254
+ .chart-range-dialog { container-type: inline-size; }
255
+ @container (max-width: 560px) {
256
+ .chart-panel .gran-toggle { display: none; }
257
+ .chart-panel .gran-select { display: inline-block; }
258
+ }
259
+ @container (max-width: 500px) {
260
+ .chart-range-dialog .mojo-daterange-with-presets { grid-template-columns: minmax(0, 1fr); }
261
+ .chart-range-dialog .mojo-calendar-presets { flex-direction: row; flex-wrap: wrap; border-right: 0; border-bottom: 1px solid var(--line); }
262
+ .chart-range-dialog .mojo-calendar-presets-eyebrow { width: 100%; }
263
+ .chart-range-dialog .mojo-calendar-presets-divider { display: none; }
264
+ }
package/src/ui/modal.tsx CHANGED
@@ -113,9 +113,12 @@ export interface ModalOptions {
113
113
  flush?: boolean;
114
114
  /** Guards Escape/backdrop dismissal. Programmatic close still succeeds. */
115
115
  canDismiss?: () => boolean;
116
+ /** Abort removes this entry immediately, even without a mounted ModalHost. */
117
+ signal?: AbortSignal;
116
118
  }
117
119
 
118
120
  function open<T>(render: (close: (value: T) => void) => ReactNode, opts: ModalOptions = {}): Promise<T | null> {
121
+ if (opts.signal?.aborted) return Promise.resolve(null);
119
122
  wireAuth();
120
123
  return new Promise<T | null>((resolve) => {
121
124
  const id = nextId++;
@@ -123,10 +126,13 @@ function open<T>(render: (close: (value: T) => void) => ReactNode, opts: ModalOp
123
126
  const close = (value: unknown) => {
124
127
  if (settled) return;
125
128
  settled = true;
129
+ opts.signal?.removeEventListener('abort', abort);
126
130
  stack = stack.filter((m) => m.id !== id);
127
131
  emit();
128
132
  resolve(value as T | null);
129
133
  };
134
+ const abort = () => close(null);
135
+ opts.signal?.addEventListener('abort', abort, { once: true });
130
136
  stack = [...stack, {
131
137
  id,
132
138
  variant: 'modal',