@devicechain/dashboards 0.21.1 → 0.22.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.js CHANGED
@@ -59,9 +59,545 @@ var WIDGET_TYPES = [
59
59
  "alarm-count",
60
60
  "command-button",
61
61
  "entity-selector",
62
- "map"
62
+ "map",
63
+ "linear-gauge",
64
+ "level-gauge",
65
+ "battery",
66
+ "status-light",
67
+ "signal-bars",
68
+ "compass",
69
+ "progress-ring",
70
+ "command-slider",
71
+ "command-toggle",
72
+ "stat-card",
73
+ "bar-chart",
74
+ "pie-chart",
75
+ "fleet-table"
63
76
  ];
77
+ var PLACEMENT_ANCHORS = [
78
+ "top-left",
79
+ "top",
80
+ "top-right",
81
+ "left",
82
+ "center",
83
+ "right",
84
+ "bottom-left",
85
+ "bottom",
86
+ "bottom-right"
87
+ ];
88
+ var PLACEMENT_MODES = ["grid", "relative"];
89
+ var RESERVED_PLACEMENT_MODES = ["pinned", "sticky"];
90
+ var THEME_COLOR_TOKENS = [
91
+ "background",
92
+ "foreground",
93
+ "card",
94
+ "card-foreground",
95
+ "muted",
96
+ "muted-foreground",
97
+ "primary",
98
+ "primary-foreground",
99
+ "border",
100
+ "destructive",
101
+ "chart-1",
102
+ "chart-2",
103
+ "chart-3",
104
+ "chart-4",
105
+ "chart-5",
106
+ "sev-critical",
107
+ "sev-major",
108
+ "sev-minor",
109
+ "sev-warning"
110
+ ];
111
+ var SPACE_TOKEN_PX = {
112
+ "space-0": 0,
113
+ "space-1": 4,
114
+ "space-2": 8,
115
+ "space-3": 12,
116
+ "space-4": 16
117
+ };
118
+ var FRAME_VARIANTS = ["card", "flat", "none"];
119
+ var FRAME_RADII = ["none", "sm", "md", "lg"];
120
+ var FRAME_BORDER_WIDTHS = [0, 1, 2, 3];
121
+ var FRAME_HEADERS = ["auto", "show", "hide"];
64
122
  var UNKNOWN_WIDGET_TYPE = "unknown-widget";
123
+ var CANVAS_DENSITIES = ["compact", "comfortable", "wall"];
124
+ var DEFAULT_CANVAS_DENSITY = "comfortable";
125
+ var DENSITY_SCALE = {
126
+ compact: 0.9,
127
+ comfortable: 1,
128
+ wall: 1.5
129
+ };
130
+ function isCanvasDensity(value) {
131
+ return typeof value === "string" && CANVAS_DENSITIES.includes(value);
132
+ }
133
+ var CURRENT_SCHEMA_VERSION = 3;
134
+
135
+ // src/grid.ts
136
+ var BASE_BREAKPOINT = "base";
137
+ var DEFAULT_GRID = Object.freeze({
138
+ columns: 12,
139
+ minCellSize: 56,
140
+ gutter: 8,
141
+ padding: 0
142
+ });
143
+ var PHONE_MIN_COLUMNS = 4;
144
+ function isSpaceToken(v) {
145
+ return typeof v === "string" && Object.prototype.hasOwnProperty.call(SPACE_TOKEN_PX, v);
146
+ }
147
+ function gridLengthPx(len, scale = 1) {
148
+ const base = typeof len === "number" ? len : SPACE_TOKEN_PX[len];
149
+ return base * scale;
150
+ }
151
+ function gutterPx(grid, scale = 1) {
152
+ const g = grid.gutter;
153
+ if (typeof g === "object") return { x: gridLengthPx(g.x, scale), y: gridLengthPx(g.y, scale) };
154
+ const v = gridLengthPx(g, scale);
155
+ return { x: v, y: v };
156
+ }
157
+ function paddingPx(grid, scale = 1) {
158
+ const p = grid.padding;
159
+ if (typeof p === "object") {
160
+ return {
161
+ top: gridLengthPx(p.top, scale),
162
+ right: gridLengthPx(p.right, scale),
163
+ bottom: gridLengthPx(p.bottom, scale),
164
+ left: gridLengthPx(p.left, scale)
165
+ };
166
+ }
167
+ const v = gridLengthPx(p, scale);
168
+ return { top: v, right: v, bottom: v, left: v };
169
+ }
170
+ function cellSizeAt(grid, containerWidth, columns = grid.columns, scale = 1) {
171
+ const pad = paddingPx(grid, scale);
172
+ const g = gutterPx(grid, scale);
173
+ return Math.max(0, (containerWidth - pad.left - pad.right - (columns - 1) * g.x) / columns);
174
+ }
175
+ function containerWidthFor(grid, cell, columns = grid.columns, scale = 1) {
176
+ const pad = paddingPx(grid, scale);
177
+ return columns * cell + (columns - 1) * gutterPx(grid, scale).x + pad.left + pad.right;
178
+ }
179
+ function breakpointMinWidth(spec) {
180
+ return typeof spec === "number" ? spec : spec.minWidth;
181
+ }
182
+ function breakpointColumns(canvas, breakpoint) {
183
+ if (breakpoint === BASE_BREAKPOINT) return canvas.grid.columns;
184
+ const spec = Object.prototype.hasOwnProperty.call(canvas.breakpoints, breakpoint) ? canvas.breakpoints[breakpoint] : void 0;
185
+ return spec !== void 0 && typeof spec === "object" ? spec.columns : canvas.grid.columns;
186
+ }
187
+ function activeBreakpoint(breakpoints, width) {
188
+ let best = BASE_BREAKPOINT;
189
+ let bestWidth = -1;
190
+ for (const [name, spec] of Object.entries(breakpoints)) {
191
+ const minWidth = breakpointMinWidth(spec);
192
+ if (width >= minWidth && minWidth > bestWidth) {
193
+ best = name;
194
+ bestWidth = minWidth;
195
+ }
196
+ }
197
+ return best;
198
+ }
199
+ function resolvePlacement(layout, breakpoint) {
200
+ return Object.prototype.hasOwnProperty.call(layout, breakpoint) ? layout[breakpoint] : layout[BASE_BREAKPOINT];
201
+ }
202
+ function layoutBreakpoints(breakpoints, widgets) {
203
+ const names = /* @__PURE__ */ new Set([BASE_BREAKPOINT, ...Object.keys(breakpoints)]);
204
+ for (const w of widgets) for (const bp of Object.keys(w.layout)) names.add(bp);
205
+ return [...names];
206
+ }
207
+ function gridFootprintsOverlap(a, b) {
208
+ return a.col < b.col + b.cols && b.col < a.col + a.cols && a.row < b.row + b.rows && b.row < a.row + a.rows;
209
+ }
210
+ function pushDown(items, first = []) {
211
+ const order = items.map((item, index) => ({ ...item, index }));
212
+ const priority = new Map(first.map((id, i) => [id, i]));
213
+ order.sort((a, b) => {
214
+ const pa = priority.get(a.id);
215
+ const pb = priority.get(b.id);
216
+ if (pa !== void 0 || pb !== void 0) return (pa ?? Infinity) - (pb ?? Infinity);
217
+ return a.placement.row - b.placement.row || a.placement.col - b.placement.col || a.index - b.index;
218
+ });
219
+ const placed = [];
220
+ const out = /* @__PURE__ */ new Map();
221
+ for (const item of order) {
222
+ let p = item.placement;
223
+ for (; ; ) {
224
+ const hits = placed.filter((q) => gridFootprintsOverlap(p, q));
225
+ if (hits.length === 0) break;
226
+ p = { ...p, row: Math.max(...hits.map((q) => q.row + q.rows)) };
227
+ }
228
+ placed.push(p);
229
+ out.set(item.id, p);
230
+ }
231
+ return out;
232
+ }
233
+ function pushAside(items, first = []) {
234
+ const order = items.map((item, index) => ({ ...item, index }));
235
+ const priority = new Map(first.map((id, i) => [id, i]));
236
+ order.sort((a, b) => {
237
+ const pa = priority.get(a.id);
238
+ const pb = priority.get(b.id);
239
+ if (pa !== void 0 || pb !== void 0) return (pa ?? Infinity) - (pb ?? Infinity);
240
+ return a.placement.row - b.placement.row || a.placement.col - b.placement.col || a.index - b.index;
241
+ });
242
+ const pending = new Map(order.map((item) => [item.id, item.placement]));
243
+ const placed = [];
244
+ const out = /* @__PURE__ */ new Map();
245
+ const clear = (p, others) => {
246
+ for (const q of others) if (gridFootprintsOverlap(p, q)) return false;
247
+ return true;
248
+ };
249
+ for (const item of order) {
250
+ pending.delete(item.id);
251
+ let p = item.placement;
252
+ if (priority.has(item.id)) {
253
+ for (; ; ) {
254
+ const hits = placed.filter((q) => gridFootprintsOverlap(p, q));
255
+ if (hits.length === 0) break;
256
+ p = { ...p, row: Math.max(...hits.map((q) => q.row + q.rows)) };
257
+ }
258
+ } else if (!clear(p, placed)) {
259
+ const from = p.row;
260
+ for (let k = 1; ; k++) {
261
+ const up = { ...p, row: from - k };
262
+ if (up.row >= 0 && clear(up, placed) && clear(up, pending.values())) {
263
+ p = up;
264
+ break;
265
+ }
266
+ const down = { ...p, row: from + k };
267
+ if (clear(down, placed)) {
268
+ p = down;
269
+ break;
270
+ }
271
+ }
272
+ }
273
+ placed.push(p);
274
+ out.set(item.id, p);
275
+ }
276
+ return out;
277
+ }
278
+ function firstFreeSlot(occupied, cols, rows, columns) {
279
+ const lastCol = Math.max(0, columns - cols);
280
+ const bottom = occupied.reduce((m, p) => Math.max(m, p.row + p.rows), 0);
281
+ for (let row = 0; row <= bottom; row++) {
282
+ for (let col = 0; col <= lastCol; col++) {
283
+ const probe = { mode: "grid", col, row, cols, rows };
284
+ if (!occupied.some((q) => gridFootprintsOverlap(probe, q))) return { col, row };
285
+ }
286
+ }
287
+ return { col: 0, row: bottom };
288
+ }
289
+ function spanPx(n, cell, gutter) {
290
+ return n * cell + (n - 1) * gutter;
291
+ }
292
+ function gridRect(p, cell, gutter) {
293
+ return {
294
+ x: p.col * (cell + gutter.x),
295
+ y: p.row * (cell + gutter.y),
296
+ w: spanPx(p.cols, cell, gutter.x),
297
+ h: spanPx(p.rows, cell, gutter.y)
298
+ };
299
+ }
300
+ function anchorFraction(anchor) {
301
+ const fx = anchor.endsWith("left") ? 0 : anchor.endsWith("right") ? 1 : 0.5;
302
+ const fy = anchor.startsWith("top") ? 0 : anchor.startsWith("bottom") ? 1 : 0.5;
303
+ return { fx, fy };
304
+ }
305
+ function relativeRect(p, host, cell, gutter) {
306
+ const w = "fill" in p.size ? host.w : spanPx(p.size.cols, cell, gutter.x);
307
+ const h = "fill" in p.size ? host.h : spanPx(p.size.rows, cell, gutter.y);
308
+ const a = anchorFraction(p.anchor);
309
+ const s = anchorFraction(p.self ?? p.anchor);
310
+ return {
311
+ x: host.x + a.fx * host.w - s.fx * w + (p.offset?.x ?? 0) * (cell + gutter.x),
312
+ y: host.y + a.fy * host.h - s.fy * h + (p.offset?.y ?? 0) * (cell + gutter.y),
313
+ w,
314
+ h
315
+ };
316
+ }
317
+ function layoutRects(widgets, breakpoint, cell, gutter) {
318
+ const rects = /* @__PURE__ */ new Map();
319
+ const relatives = [];
320
+ for (const w of widgets) {
321
+ const p = resolvePlacement(w.layout, breakpoint);
322
+ if (p.mode === "grid") rects.set(w.id, gridRect(p, cell, gutter));
323
+ else relatives.push({ id: w.id, p });
324
+ }
325
+ for (let pass = 0; pass < 2; pass++) {
326
+ for (const { id, p } of relatives) {
327
+ const host = rects.get(p.host);
328
+ if (host && !rects.has(id)) rects.set(id, relativeRect(p, host, cell, gutter));
329
+ }
330
+ }
331
+ return rects;
332
+ }
333
+ var FRAME_PRESETS = {
334
+ card: {
335
+ variant: "card",
336
+ border: { width: 1, radius: "md", color: "border" },
337
+ padding: "space-3",
338
+ background: "card",
339
+ header: "auto"
340
+ },
341
+ flat: {
342
+ variant: "flat",
343
+ border: { width: 0, radius: "md", color: "border" },
344
+ padding: "space-3",
345
+ background: "card",
346
+ header: "auto"
347
+ },
348
+ none: {
349
+ variant: "none",
350
+ border: { width: 0, radius: "none", color: "border" },
351
+ padding: "space-0",
352
+ background: "transparent",
353
+ header: "hide"
354
+ }
355
+ };
356
+ function resolveFrame(canvasFrame, widgetFrame) {
357
+ const c = canvasFrame ?? {};
358
+ const w = widgetFrame ?? {};
359
+ const variant = w.variant ?? c.variant ?? "card";
360
+ const preset = FRAME_PRESETS[variant];
361
+ return {
362
+ variant,
363
+ border: { ...preset.border, ...c.border, ...w.border },
364
+ padding: w.padding ?? c.padding ?? preset.padding,
365
+ background: w.background ?? c.background ?? preset.background,
366
+ header: w.header ?? c.header ?? preset.header
367
+ };
368
+ }
369
+
370
+ // src/is-record.ts
371
+ function isRecord(v) {
372
+ return typeof v === "object" && v !== null && !Array.isArray(v);
373
+ }
374
+
375
+ // src/legacy-grid.ts
376
+ var GRID_SCHEMA_VERSION = 2;
377
+ var LEGACY_REFERENCE_WIDTH = 1366;
378
+ var LEGACY_DEFAULTS = { columns: 24, gap: 8, rowHeight: 40 };
379
+ function num(rec, key, fallback) {
380
+ const v = rec[key];
381
+ return typeof v === "number" && Number.isFinite(v) ? v : fallback;
382
+ }
383
+ function storedSchemaVersion(raw) {
384
+ if (typeof raw.schemaVersion === "number" && Number.isFinite(raw.schemaVersion)) return raw.schemaVersion;
385
+ const grid = isRecord(raw.canvas) && isRecord(raw.canvas.grid) ? raw.canvas.grid : {};
386
+ if ("rowHeight" in grid || "gap" in grid) return 1;
387
+ const widgets = Array.isArray(raw.widgets) ? raw.widgets : [];
388
+ for (const w of widgets) {
389
+ if (!isRecord(w) || !isRecord(w.layout)) continue;
390
+ for (const box of Object.values(w.layout)) {
391
+ if (!isRecord(box)) continue;
392
+ if ("colSpan" in box || "rowSpan" in box || "z" in box) return 1;
393
+ if (!("mode" in box) && !("cols" in box)) return 1;
394
+ }
395
+ }
396
+ return CURRENT_SCHEMA_VERSION;
397
+ }
398
+ function legacyGrid(raw) {
399
+ const rec = isRecord(raw) ? raw : {};
400
+ const columns = Math.max(1, Math.round(num(rec, "columns", LEGACY_DEFAULTS.columns)));
401
+ const rowHeight = Math.max(1, num(rec, "rowHeight", LEGACY_DEFAULTS.rowHeight));
402
+ let gapRow = LEGACY_DEFAULTS.gap;
403
+ let gapCol = LEGACY_DEFAULTS.gap;
404
+ if (typeof rec.gap === "number" && Number.isFinite(rec.gap)) {
405
+ gapRow = gapCol = Math.max(0, rec.gap);
406
+ } else if (isRecord(rec.gap)) {
407
+ gapRow = Math.max(0, num(rec.gap, "row", LEGACY_DEFAULTS.gap));
408
+ gapCol = Math.max(0, num(rec.gap, "col", LEGACY_DEFAULTS.gap));
409
+ }
410
+ return {
411
+ columns,
412
+ rowPitch: rowHeight + gapRow,
413
+ gutter: gapRow === gapCol ? gapCol : { x: gapCol, y: gapRow }
414
+ };
415
+ }
416
+ function restack(items) {
417
+ const order = [...items].sort((a, b) => a.old.row - b.old.row || a.old.col - b.old.col || a.index - b.index);
418
+ const placed = [];
419
+ const moved = /* @__PURE__ */ new Map();
420
+ for (const item of order) {
421
+ let p = item.placement;
422
+ const sharesColumn = (q) => p.col < q.col + q.cols && q.col < p.col + p.cols;
423
+ const floor = placed.filter((q) => q.old.end <= item.old.row && sharesColumn(q.placement)).reduce((m, q) => Math.max(m, q.placement.row + q.placement.rows), 0);
424
+ if (floor > p.row) p = { ...p, row: floor };
425
+ for (; ; ) {
426
+ const hits = placed.filter((q) => gridFootprintsOverlap(p, q.placement));
427
+ if (hits.length === 0) break;
428
+ p = { ...p, row: Math.max(...hits.map((q) => q.placement.row + q.placement.rows)) };
429
+ }
430
+ placed.push({ ...item, placement: p });
431
+ if (p !== item.placement) moved.set(item.index, p);
432
+ }
433
+ return moved;
434
+ }
435
+ function convertLegacyDefinition(raw) {
436
+ const canvasIn = isRecord(raw.canvas) ? raw.canvas : {};
437
+ const old = legacyGrid(canvasIn.grid);
438
+ const grid = { ...DEFAULT_GRID, gutter: old.gutter };
439
+ const columns = grid.columns;
440
+ const pitch = cellSizeAt(grid, LEGACY_REFERENCE_WIDTH) + gutterPx(grid).y;
441
+ const toCols = (line) => Math.round(line * columns / old.columns);
442
+ const toRows = (line) => Math.round(line * old.rowPitch / pitch);
443
+ const convertBox = (box) => {
444
+ const col = Math.max(0, Math.round(num(box, "col", 0)));
445
+ const colSpan = Math.max(1, Math.round(num(box, "colSpan", 1)));
446
+ const row = Math.max(0, Math.round(num(box, "row", 0)));
447
+ const rowSpan = Math.max(1, Math.round(num(box, "rowSpan", 1)));
448
+ const c0 = Math.min(toCols(col), columns - 1);
449
+ const c1 = Math.min(Math.max(toCols(col + colSpan), c0 + 1), columns);
450
+ const r0 = toRows(row);
451
+ const r1 = Math.max(toRows(row + rowSpan), r0 + 1);
452
+ return { mode: "grid", col: c0, row: r0, cols: c1 - c0, rows: r1 - r0 };
453
+ };
454
+ const oldBoxes = /* @__PURE__ */ new Map();
455
+ const widgetsIn = Array.isArray(raw.widgets) ? raw.widgets : [];
456
+ const widgets = widgetsIn.map((w, i) => {
457
+ if (!isRecord(w) || !isRecord(w.layout)) return w;
458
+ const layout = {};
459
+ const olds = {};
460
+ for (const [bp, box] of Object.entries(w.layout)) {
461
+ if (!isRecord(box) || bp === "__proto__") continue;
462
+ layout[bp] = convertBox(box);
463
+ const row = Math.max(0, Math.round(num(box, "row", 0)));
464
+ olds[bp] = { row, end: row + Math.max(1, Math.round(num(box, "rowSpan", 1))), col: Math.max(0, Math.round(num(box, "col", 0))) };
465
+ }
466
+ oldBoxes.set(i, olds);
467
+ return { ...w, layout };
468
+ });
469
+ const byBreakpoint = /* @__PURE__ */ new Set([BASE_BREAKPOINT]);
470
+ for (const w of widgets) if (isRecord(w) && isRecord(w.layout)) for (const bp of Object.keys(w.layout)) byBreakpoint.add(bp);
471
+ for (const bp of byBreakpoint) {
472
+ const items = [];
473
+ widgets.forEach((w, i) => {
474
+ if (!isRecord(w) || !isRecord(w.layout)) return;
475
+ const layout = w.layout;
476
+ const own2 = Object.prototype.hasOwnProperty.call(layout, bp);
477
+ const p = own2 ? layout[bp] : layout[BASE_BREAKPOINT];
478
+ const old2 = oldBoxes.get(i)?.[own2 ? bp : BASE_BREAKPOINT];
479
+ if (p && isRecord(p) && p.mode === "grid" && old2) items.push({ index: i, placement: p, old: old2 });
480
+ });
481
+ for (const [index, moved] of restack(items)) {
482
+ const w = widgets[index];
483
+ w.layout[bp] = moved;
484
+ }
485
+ }
486
+ const breakpoints = {};
487
+ if (isRecord(canvasIn.breakpoints)) {
488
+ for (const [name, width] of Object.entries(canvasIn.breakpoints)) {
489
+ if (typeof width === "number" && Number.isFinite(width)) breakpoints[name] = width;
490
+ }
491
+ }
492
+ return {
493
+ ...raw,
494
+ schemaVersion: GRID_SCHEMA_VERSION,
495
+ canvas: { ...canvasIn, grid, breakpoints },
496
+ widgets
497
+ };
498
+ }
499
+
500
+ // src/legacy-window.ts
501
+ var TIME_SERIES = "timeseries-chart";
502
+ function convertSampleWindows(raw) {
503
+ const widgetsIn = Array.isArray(raw.widgets) ? raw.widgets : [];
504
+ const flagged = [];
505
+ const widgets = widgetsIn.map((w, i) => {
506
+ if (!isRecord(w) || w.type !== TIME_SERIES) return w;
507
+ let out = w;
508
+ if (isRecord(w.options) && Object.prototype.hasOwnProperty.call(w.options, "window")) {
509
+ const { window: _count, ...options } = w.options;
510
+ out = { ...out, options };
511
+ flagged.push(i);
512
+ }
513
+ if (isRecord(w.widget) && w.widget.optionsVersion === 1) {
514
+ out = { ...out, widget: { ...w.widget, optionsVersion: 2 } };
515
+ }
516
+ return out;
517
+ });
518
+ return { doc: { ...raw, schemaVersion: CURRENT_SCHEMA_VERSION, widgets }, flagged };
519
+ }
520
+
521
+ // src/time-range.ts
522
+ var TIME_RANGE_PRESETS = [
523
+ { id: "15m", seconds: 15 * 60 },
524
+ { id: "1h", seconds: 60 * 60 },
525
+ { id: "6h", seconds: 6 * 60 * 60 },
526
+ { id: "24h", seconds: 24 * 60 * 60 },
527
+ { id: "7d", seconds: 7 * 24 * 60 * 60 }
528
+ ];
529
+ var DEFAULT_TIME_RANGE_SECONDS = 60 * 60;
530
+ var MIN_TIME_RANGE_SECONDS = 60;
531
+ var MAX_TIME_RANGE_SECONDS = 7 * 24 * 60 * 60;
532
+ function isValidSpanSeconds(v) {
533
+ return typeof v === "number" && Number.isInteger(v) && v >= MIN_TIME_RANGE_SECONDS && v <= MAX_TIME_RANGE_SECONDS;
534
+ }
535
+ function initialTimeRange(defaultSeconds, link = {}, now = Date.now()) {
536
+ const spanSeconds = link.spanSeconds ?? (isValidSpanSeconds(defaultSeconds) ? defaultSeconds : DEFAULT_TIME_RANGE_SECONDS);
537
+ return link.paused ? { spanSeconds, paused: true, pausedAt: now } : { spanSeconds, paused: false };
538
+ }
539
+ function pauseRange(state, now) {
540
+ return state.paused ? state : { spanSeconds: state.spanSeconds, paused: true, pausedAt: now };
541
+ }
542
+ function resumeRange(state) {
543
+ return state.paused ? { spanSeconds: state.spanSeconds, paused: false } : state;
544
+ }
545
+ function withSpan(state, spanSeconds) {
546
+ return isValidSpanSeconds(spanSeconds) ? { ...state, spanSeconds } : state;
547
+ }
548
+ function rangeEnd(state, now) {
549
+ return state.paused && state.pausedAt !== void 0 ? state.pausedAt : now;
550
+ }
551
+ function rangeWindow(state, now) {
552
+ const end = rangeEnd(state, now);
553
+ return {
554
+ startTime: new Date(end - state.spanSeconds * 1e3).toISOString(),
555
+ endTime: new Date(end).toISOString()
556
+ };
557
+ }
558
+ var MAX_SEED_BUCKETS = 240;
559
+ var BASE_BUCKET_SECONDS = 60;
560
+ function bucketSecondsFor(spanSeconds) {
561
+ const wanted = Math.ceil(spanSeconds / MAX_SEED_BUCKETS);
562
+ return Math.max(BASE_BUCKET_SECONDS, Math.ceil(wanted / BASE_BUCKET_SECONDS) * BASE_BUCKET_SECONDS);
563
+ }
564
+ function seedBucketCount(spanSeconds) {
565
+ return Math.ceil(spanSeconds / bucketSecondsFor(spanSeconds));
566
+ }
567
+ function parseRangeParam(value) {
568
+ if (!value) return void 0;
569
+ return TIME_RANGE_PRESETS.find((p) => p.id === value)?.seconds;
570
+ }
571
+ function parsePausedParam(value) {
572
+ return value === "1";
573
+ }
574
+ function timeRangeFromSearch(search) {
575
+ let params;
576
+ try {
577
+ params = new URLSearchParams(search);
578
+ } catch {
579
+ return {};
580
+ }
581
+ const link = {};
582
+ const spanSeconds = parseRangeParam(params.get("range"));
583
+ if (spanSeconds !== void 0) link.spanSeconds = spanSeconds;
584
+ if (parsePausedParam(params.get("paused"))) link.paused = true;
585
+ return link;
586
+ }
587
+ function presetIdOf(spanSeconds) {
588
+ return TIME_RANGE_PRESETS.find((p) => p.seconds === spanSeconds)?.id;
589
+ }
590
+ function spanLabel(spanSeconds) {
591
+ if (spanSeconds % 86400 === 0) return `${spanSeconds / 86400}d`;
592
+ if (spanSeconds % 3600 === 0) return `${spanSeconds / 3600}h`;
593
+ if (spanSeconds % 60 === 0) return `${spanSeconds / 60}m`;
594
+ return `${spanSeconds}s`;
595
+ }
596
+ function setBoardTimeRange(def, defaultSeconds) {
597
+ const { timeRange: _previous, ...rest } = def;
598
+ if (!isValidSpanSeconds(defaultSeconds) || defaultSeconds === DEFAULT_TIME_RANGE_SECONDS) return rest;
599
+ return { ...rest, timeRange: { defaultSeconds } };
600
+ }
65
601
 
66
602
  // src/widget-compat.ts
67
603
  var BUILTIN_TYPES = new Set(WIDGET_TYPES);
@@ -72,9 +608,6 @@ function isResolvedWidget(widget) {
72
608
  return widget.type !== UNKNOWN_WIDGET_TYPE;
73
609
  }
74
610
  var SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
75
- function isRecord(v) {
76
- return typeof v === "object" && v !== null && !Array.isArray(v);
77
- }
78
611
  function parseWidgetRef(raw) {
79
612
  if (raw === void 0) return void 0;
80
613
  if (!isRecord(raw)) return null;
@@ -142,43 +675,95 @@ function unresolvedWidgets(definition) {
142
675
  }
143
676
 
144
677
  // src/definition.ts
145
- var BASE_BREAKPOINT = "base";
146
678
  var WIDGET_TYPE_SET = new Set(WIDGET_TYPES);
147
- var DEFAULT_GRID = { columns: 24, gap: 8, rowHeight: 40 };
148
679
  var DEFAULT_SIZING = "fill";
149
680
  var DashboardDefinitionError = class extends Error {
150
- constructor(message) {
681
+ constructor(message, code, found) {
151
682
  super(`invalid dashboard definition: ${message}`);
683
+ this.code = code;
684
+ this.found = found;
152
685
  this.name = "DashboardDefinitionError";
153
686
  }
154
687
  };
155
- function isRecord2(v) {
156
- return typeof v === "object" && v !== null && !Array.isArray(v);
157
- }
158
688
  function numberAt(rec, key, fallback) {
159
689
  const v = rec[key];
160
690
  return typeof v === "number" && Number.isFinite(v) ? v : fallback;
161
691
  }
162
- function parseDashboardDefinition(raw, options = {}) {
163
- if (!isRecord2(raw)) throw new DashboardDefinitionError("not a JSON object");
692
+ function parseDashboardDefinition(input, options = {}) {
693
+ if (!isRecord(input)) throw new DashboardDefinitionError("not a JSON object");
694
+ const stored = storedSchemaVersion(input);
695
+ if (stored > CURRENT_SCHEMA_VERSION) {
696
+ throw new DashboardDefinitionError(`schemaVersion ${stored} is too new`, "schemaTooNew", stored);
697
+ }
698
+ if (!Array.isArray(input.widgets)) throw new DashboardDefinitionError("widgets must be an array");
699
+ const legacy = stored < CURRENT_SCHEMA_VERSION;
700
+ const gridConverted = stored < GRID_SCHEMA_VERSION;
701
+ let raw = input;
702
+ if (gridConverted) raw = convertLegacyDefinition(raw);
703
+ let flaggedWindows = [];
704
+ if (legacy) {
705
+ const converted = convertSampleWindows(raw);
706
+ raw = converted.doc;
707
+ flaggedWindows = converted.flagged;
708
+ }
164
709
  const widgetsRaw = raw.widgets;
165
- if (!Array.isArray(widgetsRaw)) throw new DashboardDefinitionError("widgets must be an array");
166
710
  const def = {
167
- schemaVersion: numberAt(raw, "schemaVersion", 1),
711
+ // The format this release writes (a newer one was refused above): a save writes that format.
712
+ schemaVersion: CURRENT_SCHEMA_VERSION,
168
713
  title: typeof raw.title === "string" ? raw.title : "",
169
714
  canvas: parseCanvas(raw.canvas),
170
715
  widgets: widgetsRaw.map((w, i) => parseWidget(w, i, options.catalog))
171
716
  };
172
717
  const slots = parseSlots(raw.slots);
173
718
  if (slots) def.slots = slots;
719
+ const timeRange = parseBoardTimeRange(raw.timeRange);
720
+ if (timeRange) def.timeRange = timeRange;
721
+ validatePlacements(def);
722
+ const flaggedIds = flaggedWindows.map((i) => def.widgets[i]?.id).filter((id) => id !== void 0);
723
+ if (gridConverted && def.widgets.length > 0 || flaggedIds.length > 0) def.convertedFrom = stored;
724
+ else if (typeof raw.convertedFrom === "number") def.convertedFrom = raw.convertedFrom;
725
+ if (flaggedIds.length > 0) def.convertedWindows = flaggedIds;
726
+ else if (Array.isArray(raw.convertedWindows) && raw.convertedWindows.every((id) => typeof id === "string")) {
727
+ def.convertedWindows = raw.convertedWindows;
728
+ }
174
729
  return def;
175
730
  }
731
+ function validatePlacements(def) {
732
+ for (const bp of layoutBreakpoints(def.canvas.breakpoints, def.widgets)) {
733
+ const at = bp === BASE_BREAKPOINT ? "" : ` at breakpoint '${bp}'`;
734
+ const placements = /* @__PURE__ */ new Map();
735
+ for (const w of def.widgets) placements.set(w.id, resolvePlacement(w.layout, bp));
736
+ const grid = [...placements].filter((e) => e[1].mode === "grid");
737
+ for (let i = 0; i < grid.length; i++) {
738
+ for (let j = i + 1; j < grid.length; j++) {
739
+ if (gridFootprintsOverlap(grid[i][1], grid[j][1])) {
740
+ throw new DashboardDefinitionError(
741
+ `widgets '${grid[i][0]}' and '${grid[j][0]}' overlap on the grid${at}; a widget that is meant to sit over another is placed relative to it`
742
+ );
743
+ }
744
+ }
745
+ }
746
+ for (const [id, p] of placements) {
747
+ if (p.mode !== "relative") continue;
748
+ if (p.host === id) throw new DashboardDefinitionError(`widget '${id}' is placed relative to itself${at}`);
749
+ const host = placements.get(p.host);
750
+ if (!host) {
751
+ throw new DashboardDefinitionError(`widget '${id}' is placed relative to '${p.host}', which does not exist${at}`);
752
+ }
753
+ if (host.mode === "relative" && placements.get(host.host)?.mode !== "grid") {
754
+ throw new DashboardDefinitionError(
755
+ `widget '${id}' is placed relative to '${p.host}', which is relative to another relative widget${at}; a relative widget's host may be relative only when that host sits on a grid widget`
756
+ );
757
+ }
758
+ }
759
+ }
760
+ }
176
761
  function parseSlots(raw) {
177
- if (!isRecord2(raw)) return void 0;
762
+ if (!isRecord(raw)) return void 0;
178
763
  const slots = {};
179
764
  for (const [name, spec] of Object.entries(raw)) {
180
765
  if (name === "__proto__") continue;
181
- if (!isRecord2(spec)) continue;
766
+ if (!isRecord(spec)) continue;
182
767
  const type = spec.type === "anchor" ? "anchor" : "device";
183
768
  const slot = { type };
184
769
  if (typeof spec.label === "string") slot.label = spec.label;
@@ -193,7 +778,7 @@ function parseSlots(raw) {
193
778
  return slots;
194
779
  }
195
780
  function parseScope(raw) {
196
- if (!isRecord2(raw)) return void 0;
781
+ if (!isRecord(raw)) return void 0;
197
782
  const parent = typeof raw.parent === "string" ? raw.parent : "";
198
783
  if (!parent) return void 0;
199
784
  return { parent, strategy: raw.strategy === "manual" ? "manual" : "first" };
@@ -237,13 +822,13 @@ function canScopeSlot(slots, child, parentName) {
237
822
  return true;
238
823
  }
239
824
  function parseSlotBinding(raw) {
240
- if (!isRecord2(raw)) return void 0;
825
+ if (!isRecord(raw)) return void 0;
241
826
  if (raw.kind === "device") {
242
827
  const token = stringAt(raw, "deviceToken");
243
828
  return token ? { kind: "device", deviceToken: token } : void 0;
244
829
  }
245
830
  if (raw.kind === "anchor") {
246
- const anchorRec = isRecord2(raw.anchor) ? raw.anchor : {};
831
+ const anchorRec = isRecord(raw.anchor) ? raw.anchor : {};
247
832
  const targetToken = stringAt(anchorRec, "targetToken");
248
833
  if (!targetToken) return void 0;
249
834
  return {
@@ -257,23 +842,86 @@ function parseSlotBinding(raw) {
257
842
  }
258
843
  return void 0;
259
844
  }
845
+ function parseGridLength(raw, fallback, where) {
846
+ if (raw === void 0) return fallback;
847
+ if (typeof raw === "number" && Number.isFinite(raw)) return Math.max(0, raw);
848
+ if (isSpaceToken(raw)) return raw;
849
+ throw new DashboardDefinitionError(`${where} is ${JSON.stringify(raw)}; a length is a number of px or a space-0..space-4 token`);
850
+ }
260
851
  function parseGrid(raw) {
261
- const rec = isRecord2(raw) ? raw : {};
852
+ const rec = isRecord(raw) ? raw : {};
262
853
  const columns = Math.max(1, Math.round(numberAt(rec, "columns", DEFAULT_GRID.columns)));
263
- const rowHeight = Math.max(1, numberAt(rec, "rowHeight", DEFAULT_GRID.rowHeight));
264
- let gap = DEFAULT_GRID.gap;
265
- if (typeof rec.gap === "number" && Number.isFinite(rec.gap)) {
266
- gap = Math.max(0, rec.gap);
267
- } else if (isRecord2(rec.gap)) {
268
- gap = {
269
- row: Math.max(0, numberAt(rec.gap, "row", DEFAULT_GRID.gap)),
270
- col: Math.max(0, numberAt(rec.gap, "col", DEFAULT_GRID.gap))
271
- };
854
+ const minCellSize = Math.max(1, numberAt(rec, "minCellSize", DEFAULT_GRID.minCellSize));
855
+ const dg = DEFAULT_GRID.gutter;
856
+ const gutter = isRecord(rec.gutter) ? {
857
+ x: parseGridLength(rec.gutter.x, dg, "canvas.grid.gutter.x"),
858
+ y: parseGridLength(rec.gutter.y, dg, "canvas.grid.gutter.y")
859
+ } : parseGridLength(rec.gutter, dg, "canvas.grid.gutter");
860
+ const dp = DEFAULT_GRID.padding;
861
+ const padding = isRecord(rec.padding) ? {
862
+ top: parseGridLength(rec.padding.top, dp, "canvas.grid.padding.top"),
863
+ right: parseGridLength(rec.padding.right, dp, "canvas.grid.padding.right"),
864
+ bottom: parseGridLength(rec.padding.bottom, dp, "canvas.grid.padding.bottom"),
865
+ left: parseGridLength(rec.padding.left, dp, "canvas.grid.padding.left")
866
+ } : parseGridLength(rec.padding, dp, "canvas.grid.padding");
867
+ return { columns, minCellSize, gutter, padding };
868
+ }
869
+ function parseBreakpoints(raw, grid) {
870
+ const rec = isRecord(raw) ? raw : {};
871
+ const breakpoints = {};
872
+ for (const [name, spec] of Object.entries(rec)) {
873
+ if (name === "__proto__") continue;
874
+ if (typeof spec === "number" && Number.isFinite(spec)) {
875
+ breakpoints[name] = spec;
876
+ } else if (isRecord(spec) && typeof spec.minWidth === "number" && Number.isFinite(spec.minWidth) && typeof spec.columns === "number" && Number.isInteger(spec.columns) && spec.columns >= 1) {
877
+ breakpoints[name] = { minWidth: spec.minWidth, columns: spec.columns };
878
+ }
879
+ }
880
+ const base = breakpoints[BASE_BREAKPOINT];
881
+ if (base === void 0) breakpoints[BASE_BREAKPOINT] = 0;
882
+ else if (typeof base === "object" && base.columns !== grid.columns) {
883
+ throw new DashboardDefinitionError(
884
+ `breakpoint '${BASE_BREAKPOINT}' declares ${base.columns} columns; the base layout is in canvas.grid.columns (${grid.columns})`
885
+ );
272
886
  }
273
- return { columns, gap, rowHeight };
887
+ return breakpoints;
888
+ }
889
+ function oneOf(values, raw, where) {
890
+ if (raw === void 0) return void 0;
891
+ if (values.includes(raw)) return raw;
892
+ throw new DashboardDefinitionError(`${where} is ${JSON.stringify(raw)}; expected one of ${values.map((v) => JSON.stringify(v)).join(", ")}`);
893
+ }
894
+ function parseFrame(raw, where) {
895
+ if (raw === void 0) return void 0;
896
+ if (!isRecord(raw)) throw new DashboardDefinitionError(`${where} is not an object`);
897
+ const frame = {};
898
+ const variant = oneOf(FRAME_VARIANTS, raw.variant, `${where}.variant`);
899
+ if (variant) frame.variant = variant;
900
+ if (raw.border !== void 0) {
901
+ if (!isRecord(raw.border)) throw new DashboardDefinitionError(`${where}.border is not an object`);
902
+ const border = {};
903
+ const width = oneOf(FRAME_BORDER_WIDTHS, raw.border.width, `${where}.border.width`);
904
+ if (width !== void 0) border.width = width;
905
+ const radius = oneOf(FRAME_RADII, raw.border.radius, `${where}.border.radius`);
906
+ if (radius) border.radius = radius;
907
+ const color = oneOf(THEME_COLOR_TOKENS, raw.border.color, `${where}.border.color`);
908
+ if (color) border.color = color;
909
+ frame.border = border;
910
+ }
911
+ if (raw.padding !== void 0) {
912
+ if (!isSpaceToken(raw.padding)) {
913
+ throw new DashboardDefinitionError(`${where}.padding is ${JSON.stringify(raw.padding)}; expected a space-0..space-4 token`);
914
+ }
915
+ frame.padding = raw.padding;
916
+ }
917
+ const background = oneOf([...THEME_COLOR_TOKENS, "transparent"], raw.background, `${where}.background`);
918
+ if (background) frame.background = background;
919
+ const header = oneOf(FRAME_HEADERS, raw.header, `${where}.header`);
920
+ if (header) frame.header = header;
921
+ return frame;
274
922
  }
275
923
  function parseSizing(raw) {
276
- if (isRecord2(raw)) {
924
+ if (isRecord(raw)) {
277
925
  if (typeof raw.width === "number" && Number.isFinite(raw.width)) {
278
926
  return { width: Math.max(1, raw.width) };
279
927
  }
@@ -283,18 +931,20 @@ function parseSizing(raw) {
283
931
  }
284
932
  return DEFAULT_SIZING;
285
933
  }
934
+ function parseBoardTimeRange(raw) {
935
+ if (!isRecord(raw) || !isValidSpanSeconds(raw.defaultSeconds)) return void 0;
936
+ return { defaultSeconds: raw.defaultSeconds };
937
+ }
286
938
  function parseCanvas(raw) {
287
- const rec = isRecord2(raw) ? raw : {};
939
+ const rec = isRecord(raw) ? raw : {};
288
940
  const grid = parseGrid(rec.grid);
289
941
  const sizing = parseSizing(rec.sizing);
290
- const bpRec = isRecord2(rec.breakpoints) ? rec.breakpoints : {};
291
- const breakpoints = {};
292
- for (const [name, width] of Object.entries(bpRec)) {
293
- if (typeof width === "number" && Number.isFinite(width)) breakpoints[name] = width;
294
- }
295
- if (!(BASE_BREAKPOINT in breakpoints)) breakpoints[BASE_BREAKPOINT] = 0;
942
+ const breakpoints = parseBreakpoints(rec.breakpoints, grid);
296
943
  const canvas = { grid, sizing, breakpoints };
297
- if (isRecord2(rec.background)) {
944
+ const frame = parseFrame(rec.frame, "canvas.frame");
945
+ if (frame) canvas.frame = frame;
946
+ if (isCanvasDensity(rec.density)) canvas.density = rec.density;
947
+ if (isRecord(rec.background)) {
298
948
  const { color, imageUrl } = rec.background;
299
949
  canvas.background = {
300
950
  color: typeof color === "string" ? color : null,
@@ -304,12 +954,12 @@ function parseCanvas(raw) {
304
954
  return canvas;
305
955
  }
306
956
  function parseWidget(raw, index, catalog) {
307
- if (!isRecord2(raw)) throw new DashboardDefinitionError(`widgets[${index}] is not an object`);
957
+ if (!isRecord(raw)) throw new DashboardDefinitionError(`widgets[${index}] is not an object`);
308
958
  const type = raw.type;
309
959
  if (typeof type !== "string" || type.length === 0) {
310
960
  throw new DashboardDefinitionError(`widgets[${index}] has unknown type ${JSON.stringify(type)}`);
311
961
  }
312
- if (type === UNKNOWN_WIDGET_TYPE && isRecord2(raw.raw)) return reparseUnknownWidget(raw, index, catalog);
962
+ if (type === UNKNOWN_WIDGET_TYPE && isRecord(raw.raw)) return reparseUnknownWidget(raw, index, catalog);
313
963
  const builtin = WIDGET_TYPE_SET.has(type);
314
964
  const reason = unresolvedReason(type, raw.widget, catalog, builtin) ?? (builtin ? void 0 : reservedFieldReason(type, raw));
315
965
  if (reason) return parseUnknownWidget(raw, index, reason);
@@ -328,11 +978,13 @@ function parseWidget(raw, index, catalog) {
328
978
  };
329
979
  const ds = parseDatasource(raw.datasource, index);
330
980
  if (ds) widget.datasource = ds;
331
- if (isRecord2(raw.options)) widget.options = raw.options;
981
+ if (isRecord(raw.options)) widget.options = raw.options;
982
+ const frame = parseFrame(raw.frame, `widgets[${index}].frame`);
983
+ if (frame) widget.frame = frame;
332
984
  if (!builtin) return withStoredFields(raw, widget);
333
985
  return widget;
334
986
  }
335
- var OWNED_WIDGET_KEYS = /* @__PURE__ */ new Set(["id", "type", "widget", "layout", "datasource", "options"]);
987
+ var OWNED_WIDGET_KEYS = /* @__PURE__ */ new Set(["id", "type", "widget", "layout", "datasource", "options", "frame"]);
336
988
  function withStoredFields(raw, parsed) {
337
989
  const owned = parsed;
338
990
  const out = {};
@@ -387,7 +1039,7 @@ function stringArrayAt(rec, key) {
387
1039
  return Array.isArray(v) ? v.filter((m) => typeof m === "string") : [];
388
1040
  }
389
1041
  function parseLocationSelection(raw) {
390
- if (!isRecord2(raw)) return void 0;
1042
+ if (!isRecord(raw)) return void 0;
391
1043
  return raw.series === "latest" ? { series: "latest" } : void 0;
392
1044
  }
393
1045
  function withLocation(selector, raw) {
@@ -395,7 +1047,7 @@ function withLocation(selector, raw) {
395
1047
  return location ? { ...selector, location } : selector;
396
1048
  }
397
1049
  function parseDatasource(raw, index) {
398
- if (!isRecord2(raw)) return void 0;
1050
+ if (!isRecord(raw)) return void 0;
399
1051
  const kind = raw.kind;
400
1052
  if (typeof kind !== "string" || kind.length === 0) return void 0;
401
1053
  if (kind === "unbound") {
@@ -410,7 +1062,7 @@ function parseDatasource(raw, index) {
410
1062
  );
411
1063
  }
412
1064
  if (kind === "anchor") {
413
- const anchorRec = isRecord2(raw.anchor) ? raw.anchor : {};
1065
+ const anchorRec = isRecord(raw.anchor) ? raw.anchor : {};
414
1066
  const selector = withLocation(
415
1067
  {
416
1068
  kind: "anchor",
@@ -425,7 +1077,7 @@ function parseDatasource(raw, index) {
425
1077
  },
426
1078
  raw
427
1079
  );
428
- if (isRecord2(raw.aggregation)) {
1080
+ if (isRecord(raw.aggregation)) {
429
1081
  selector.aggregation = raw.aggregation;
430
1082
  }
431
1083
  return selector;
@@ -439,36 +1091,88 @@ function parseDatasource(raw, index) {
439
1091
  return raw;
440
1092
  }
441
1093
  function parseLayout(raw, index) {
442
- if (!isRecord2(raw)) throw new DashboardDefinitionError(`widgets[${index}].layout is missing`);
1094
+ if (!isRecord(raw)) throw new DashboardDefinitionError(`widgets[${index}].layout is missing`);
443
1095
  const layout = {};
444
- for (const [bp, box] of Object.entries(raw)) {
445
- if (isRecord2(box)) layout[bp] = parseBox(box);
1096
+ for (const [bp, placement] of Object.entries(raw)) {
1097
+ if (bp === "__proto__") continue;
1098
+ if (isRecord(placement)) layout[bp] = parsePlacement(placement, `widgets[${index}].layout.${bp}`);
446
1099
  }
447
- if (!(BASE_BREAKPOINT in layout)) {
448
- throw new DashboardDefinitionError(`widgets[${index}].layout has no '${BASE_BREAKPOINT}' box`);
1100
+ if (!Object.prototype.hasOwnProperty.call(layout, BASE_BREAKPOINT)) {
1101
+ throw new DashboardDefinitionError(`widgets[${index}].layout has no '${BASE_BREAKPOINT}' placement`);
449
1102
  }
450
1103
  return layout;
451
1104
  }
452
- function parseBox(rec) {
453
- const box = {
454
- col: Math.max(0, Math.round(numberAt(rec, "col", 0))),
455
- colSpan: Math.max(1, Math.round(numberAt(rec, "colSpan", 1))),
456
- row: Math.max(0, Math.round(numberAt(rec, "row", 0))),
457
- rowSpan: Math.max(1, Math.round(numberAt(rec, "rowSpan", 1))),
458
- // z rounds too: a fractional zIndex is invalid CSS and silently drops to auto,
459
- // so keep it an integer like every other box field.
460
- z: Math.round(numberAt(rec, "z", 0))
1105
+ function cellCount(rec, key, min, where) {
1106
+ const v = rec[key];
1107
+ if (typeof v !== "number" || !Number.isFinite(v)) {
1108
+ throw new DashboardDefinitionError(`${where}.${key} is ${JSON.stringify(v)}; expected a number of cells`);
1109
+ }
1110
+ return Math.max(min, Math.round(v));
1111
+ }
1112
+ function parsePlacement(rec, where) {
1113
+ const mode = rec.mode ?? "grid";
1114
+ if (mode === "grid") {
1115
+ return {
1116
+ mode: "grid",
1117
+ col: cellCount(rec, "col", 0, where),
1118
+ row: cellCount(rec, "row", 0, where),
1119
+ cols: cellCount(rec, "cols", 1, where),
1120
+ rows: cellCount(rec, "rows", 1, where)
1121
+ };
1122
+ }
1123
+ if (mode === "relative") return parseRelative(rec, where);
1124
+ if (RESERVED_PLACEMENT_MODES.includes(mode)) {
1125
+ throw new DashboardDefinitionError(`${where}.mode '${String(mode)}' is reserved and not supported by this release`);
1126
+ }
1127
+ throw new DashboardDefinitionError(`${where}.mode is ${JSON.stringify(mode)}; expected 'grid' or 'relative'`);
1128
+ }
1129
+ function parseAnchor(raw, where) {
1130
+ if (PLACEMENT_ANCHORS.includes(raw)) return raw;
1131
+ throw new DashboardDefinitionError(`${where} is ${JSON.stringify(raw)}; expected one of ${PLACEMENT_ANCHORS.join(", ")}`);
1132
+ }
1133
+ function parseRelative(rec, where) {
1134
+ if (typeof rec.host !== "string" || rec.host.length === 0) {
1135
+ throw new DashboardDefinitionError(`${where}.host is ${JSON.stringify(rec.host)}; a relative widget names the widget it is placed against`);
1136
+ }
1137
+ const placement = {
1138
+ mode: "relative",
1139
+ host: rec.host,
1140
+ anchor: parseAnchor(rec.anchor, `${where}.anchor`),
1141
+ size: { fill: "host" }
461
1142
  };
462
- if (isRecord2(rec.offset)) {
463
- box.offset = { x: numberAt(rec.offset, "x", 0), y: numberAt(rec.offset, "y", 0) };
1143
+ if (rec.self !== void 0) placement.self = parseAnchor(rec.self, `${where}.self`);
1144
+ if (rec.offset !== void 0) {
1145
+ const o = rec.offset;
1146
+ if (!isRecord(o) || !isFiniteNumber(o.x) || !isFiniteNumber(o.y)) {
1147
+ throw new DashboardDefinitionError(`${where}.offset is ${JSON.stringify(o)}; expected {x, y} in cells`);
1148
+ }
1149
+ placement.offset = { x: o.x, y: o.y };
464
1150
  }
465
- return box;
1151
+ const size = rec.size;
1152
+ if (isRecord(size) && size.fill === "host") {
1153
+ placement.size = { fill: "host" };
1154
+ } else if (isRecord(size)) {
1155
+ placement.size = { cols: cellCount(size, "cols", 1, `${where}.size`), rows: cellCount(size, "rows", 1, `${where}.size`) };
1156
+ } else {
1157
+ throw new DashboardDefinitionError(`${where}.size is ${JSON.stringify(size)}; expected {cols, rows} or {fill: 'host'}`);
1158
+ }
1159
+ if (rec.layer !== void 0) {
1160
+ if (!isFiniteNumber(rec.layer) || !Number.isInteger(rec.layer)) {
1161
+ throw new DashboardDefinitionError(`${where}.layer is ${JSON.stringify(rec.layer)}; expected a whole number`);
1162
+ }
1163
+ placement.layer = rec.layer;
1164
+ }
1165
+ return placement;
1166
+ }
1167
+ function isFiniteNumber(v) {
1168
+ return typeof v === "number" && Number.isFinite(v);
466
1169
  }
467
1170
  function storedDefinition(def) {
468
- if (!def.widgets.some((w) => w.type === UNKNOWN_WIDGET_TYPE)) return def;
1171
+ const unmarked = withoutConversionMark(def);
1172
+ if (!unmarked.widgets.some((w) => w.type === UNKNOWN_WIDGET_TYPE)) return unmarked;
469
1173
  return {
470
- ...def,
471
- widgets: def.widgets.map((w) => w.type === UNKNOWN_WIDGET_TYPE ? storedUnknownWidget(w) : w)
1174
+ ...unmarked,
1175
+ widgets: unmarked.widgets.map((w) => w.type === UNKNOWN_WIDGET_TYPE ? storedUnknownWidget(w) : w)
472
1176
  };
473
1177
  }
474
1178
  function storedUnknownWidget(w) {
@@ -480,11 +1184,11 @@ function storedUnknownWidget(w) {
480
1184
  return out;
481
1185
  }
482
1186
  function storedUnknownLayout(layout, rawLayout) {
483
- const stored = isRecord2(rawLayout) ? rawLayout : {};
1187
+ const stored = isRecord(rawLayout) ? rawLayout : {};
484
1188
  const out = {};
485
- let changed = !isRecord2(rawLayout);
1189
+ let changed = !isRecord(rawLayout);
486
1190
  for (const [bp, box] of Object.entries(stored)) {
487
- if (!isRecord2(box)) {
1191
+ if (!isRecord(box)) {
488
1192
  out[bp] = box;
489
1193
  continue;
490
1194
  }
@@ -493,48 +1197,52 @@ function storedUnknownLayout(layout, rawLayout) {
493
1197
  changed = true;
494
1198
  continue;
495
1199
  }
496
- if (sameBox(parseBox(box), current)) {
1200
+ if (samePlacement(parsePlacement(box, bp), current)) {
497
1201
  out[bp] = box;
498
1202
  } else {
499
- out[bp] = movedBox(box, current);
1203
+ out[bp] = movedPlacement(box, current);
500
1204
  changed = true;
501
1205
  }
502
1206
  }
503
1207
  for (const [bp, box] of Object.entries(layout)) {
504
- if (Object.prototype.hasOwnProperty.call(stored, bp) && isRecord2(stored[bp])) continue;
1208
+ if (Object.prototype.hasOwnProperty.call(stored, bp) && isRecord(stored[bp])) continue;
505
1209
  out[bp] = box;
506
1210
  changed = true;
507
1211
  }
508
1212
  return changed ? out : rawLayout;
509
1213
  }
510
- function movedBox(stored, current) {
511
- const out = { ...stored, ...current };
512
- if (current.offset === void 0) delete out.offset;
1214
+ var PLACEMENT_KEYS = /* @__PURE__ */ new Set([
1215
+ "mode",
1216
+ "col",
1217
+ "row",
1218
+ "cols",
1219
+ "rows",
1220
+ "host",
1221
+ "anchor",
1222
+ "self",
1223
+ "offset",
1224
+ "size",
1225
+ "layer"
1226
+ ]);
1227
+ function movedPlacement(stored, current) {
1228
+ const out = { ...current };
1229
+ for (const [k, v] of Object.entries(stored)) if (!PLACEMENT_KEYS.has(k)) out[k] = v;
513
1230
  return out;
514
1231
  }
515
- function sameBox(a, b) {
516
- return a.col === b.col && a.colSpan === b.colSpan && a.row === b.row && a.rowSpan === b.rowSpan && a.z === b.z && a.offset?.x === b.offset?.x && a.offset?.y === b.offset?.y;
1232
+ function samePlacement(a, b) {
1233
+ return JSON.stringify(a) === JSON.stringify(b);
517
1234
  }
518
1235
  function serializeDefinition(def) {
519
1236
  return JSON.stringify(storedDefinition(def));
520
1237
  }
1238
+ function withoutConversionMark(def) {
1239
+ if (def.convertedFrom === void 0 && def.convertedWindows === void 0) return def;
1240
+ const { convertedFrom: _mark, convertedWindows: _windows, ...rest } = def;
1241
+ return rest;
1242
+ }
521
1243
  function isDirty(a, b) {
522
1244
  return serializeDefinition(a) !== serializeDefinition(b);
523
1245
  }
524
- function resolveWidgetBox(layout, breakpoint) {
525
- return layout[breakpoint] ?? layout[BASE_BREAKPOINT];
526
- }
527
- function activeBreakpoint(breakpoints, viewportWidth) {
528
- let best = BASE_BREAKPOINT;
529
- let bestWidth = -1;
530
- for (const [name, minWidth] of Object.entries(breakpoints)) {
531
- if (viewportWidth >= minWidth && minWidth > bestWidth) {
532
- best = name;
533
- bestWidth = minWidth;
534
- }
535
- }
536
- return best;
537
- }
538
1246
  var idCounter = 0;
539
1247
  function generateWidgetId() {
540
1248
  const c = globalThis.crypto;
@@ -901,6 +1609,9 @@ var DEVICES_FOR_ANCHOR = `
901
1609
  token
902
1610
  }
903
1611
  }
1612
+ pagination {
1613
+ totalRecords
1614
+ }
904
1615
  }
905
1616
  }
906
1617
  `;
@@ -911,6 +1622,14 @@ var DEVICES_BY_TOKEN = `
911
1622
  }
912
1623
  }
913
1624
  `;
1625
+ var DEVICE_NAMES = `
1626
+ query DashboardDeviceNames($tokens: [String!]!) {
1627
+ devicesByToken(tokens: $tokens) {
1628
+ token
1629
+ name
1630
+ }
1631
+ }
1632
+ `;
914
1633
  var LIST_DEVICES = `
915
1634
  query DashboardListDevices($criteria: DeviceSearchCriteria!) {
916
1635
  devices(criteria: $criteria) {
@@ -957,6 +1676,10 @@ var BUCKETED_MEASUREMENTS = `
957
1676
  bucketStart
958
1677
  name
959
1678
  avg
1679
+ min
1680
+ max
1681
+ sum
1682
+ count
960
1683
  }
961
1684
  }
962
1685
  `;
@@ -993,6 +1716,104 @@ function fetchKind(kind, criteria) {
993
1716
  }
994
1717
  }
995
1718
 
1719
+ // src/same-label.ts
1720
+ function sameLabel(a, b) {
1721
+ if (!a || !b) return false;
1722
+ const x = a.trim().toLowerCase();
1723
+ return x !== "" && x === b.trim().toLowerCase();
1724
+ }
1725
+
1726
+ // src/connection.ts
1727
+ var OFFLINE_AFTER_MS = 2e4;
1728
+ var STABLE_AFTER_MS = 1e4;
1729
+ var ConnectionTracker = class {
1730
+ constructor(offlineAfterMs = OFFLINE_AFTER_MS, stableAfterMs = STABLE_AFTER_MS) {
1731
+ this.offlineAfterMs = offlineAfterMs;
1732
+ this.stableAfterMs = stableAfterMs;
1733
+ this.down = /* @__PURE__ */ new Set();
1734
+ this.listeners = /* @__PURE__ */ new Set();
1735
+ this.escalated = false;
1736
+ this.browserOffline = false;
1737
+ this.current = "live";
1738
+ }
1739
+ get state() {
1740
+ return this.current;
1741
+ }
1742
+ register() {
1743
+ const handle = {
1744
+ connected: () => {
1745
+ this.down.delete(handle);
1746
+ this.settle();
1747
+ },
1748
+ closed: () => {
1749
+ this.down.add(handle);
1750
+ this.settle();
1751
+ },
1752
+ release: () => {
1753
+ this.down.delete(handle);
1754
+ this.settle();
1755
+ }
1756
+ };
1757
+ return handle;
1758
+ }
1759
+ // Listeners are what keep the browser online/offline hooks attached, so a hub nobody is
1760
+ // watching holds no window listeners. Returns the unsubscribe.
1761
+ subscribe(listener) {
1762
+ this.listeners.add(listener);
1763
+ if (this.listeners.size === 1) this.attach();
1764
+ return () => {
1765
+ this.listeners.delete(listener);
1766
+ if (this.listeners.size === 0) this.release();
1767
+ };
1768
+ }
1769
+ attach() {
1770
+ if (typeof window === "undefined") return;
1771
+ const sync = () => {
1772
+ this.browserOffline = typeof navigator !== "undefined" && navigator.onLine === false;
1773
+ this.settle();
1774
+ };
1775
+ window.addEventListener("offline", sync);
1776
+ window.addEventListener("online", sync);
1777
+ this.detach = () => {
1778
+ window.removeEventListener("offline", sync);
1779
+ window.removeEventListener("online", sync);
1780
+ };
1781
+ sync();
1782
+ }
1783
+ release() {
1784
+ this.detach?.();
1785
+ this.detach = void 0;
1786
+ this.browserOffline = false;
1787
+ this.settle();
1788
+ }
1789
+ settle() {
1790
+ if (this.down.size === 0) {
1791
+ if ((this.escalate || this.escalated) && !this.recover) {
1792
+ this.recover = setTimeout(() => {
1793
+ this.recover = void 0;
1794
+ if (this.escalate) clearTimeout(this.escalate);
1795
+ this.escalate = void 0;
1796
+ this.escalated = false;
1797
+ }, this.stableAfterMs);
1798
+ }
1799
+ } else {
1800
+ if (this.recover) clearTimeout(this.recover);
1801
+ this.recover = void 0;
1802
+ }
1803
+ if (this.down.size > 0 && !this.escalate && !this.escalated) {
1804
+ this.escalate = setTimeout(() => {
1805
+ this.escalate = void 0;
1806
+ this.escalated = true;
1807
+ this.settle();
1808
+ }, this.offlineAfterMs);
1809
+ }
1810
+ const next = this.browserOffline ? "offline" : this.down.size === 0 ? "live" : this.escalated ? "offline" : "reconnecting";
1811
+ if (next === this.current) return;
1812
+ this.current = next;
1813
+ for (const listener of [...this.listeners]) listener();
1814
+ }
1815
+ };
1816
+
996
1817
  // src/hub.ts
997
1818
  import { gql as gql2, isForbiddenError, subscribe } from "@devicechain/client";
998
1819
 
@@ -1109,6 +1930,34 @@ var LATEST_LOCATIONS_QUERY = `
1109
1930
  }
1110
1931
  `;
1111
1932
 
1933
+ // src/members.ts
1934
+ var MEMBER_SEED_CONCURRENCY = 6;
1935
+ var SEED_CANCELLED = /* @__PURE__ */ Symbol("member seed cancelled");
1936
+ function createLimiter(max) {
1937
+ let active = 0;
1938
+ const queue = [];
1939
+ const next = () => {
1940
+ while (active < max && queue.length > 0) queue.shift()();
1941
+ };
1942
+ return function run(task, cancelled = () => false) {
1943
+ return new Promise((resolve, reject) => {
1944
+ const start = () => {
1945
+ if (cancelled()) {
1946
+ reject(SEED_CANCELLED);
1947
+ return;
1948
+ }
1949
+ active += 1;
1950
+ task().then(resolve, reject).finally(() => {
1951
+ active -= 1;
1952
+ next();
1953
+ });
1954
+ };
1955
+ queue.push(start);
1956
+ next();
1957
+ });
1958
+ };
1959
+ }
1960
+
1112
1961
  // src/internal/measurement-doc.ts
1113
1962
  var MEASUREMENT_STREAM = `
1114
1963
  subscription MeasurementStream($deviceToken: String, $name: String) {
@@ -1161,9 +2010,32 @@ var DashboardHub = class {
1161
2010
  // commandDisposers — the location channel holds a poll per subscription, not a shared
1162
2011
  // device stream), so disposeAll() can tear down every map widget's poll.
1163
2012
  this.locationDisposers = /* @__PURE__ */ new Set();
2013
+ // Which of this hub's sockets are down, folded into one state a header can show.
2014
+ this.connection = new ConnectionTracker();
2015
+ // Streams evicted by an upstream error that still have subscribers: out of `streams`, but
2016
+ // still holding their connection handle (see ensureStream), so disposeAll() can reach them.
2017
+ this.deadStreams = /* @__PURE__ */ new Set();
2018
+ // The hub-wide cap on in-flight member seed reads, and the reads currently in flight (so a second
2019
+ // widget asking the same question of the same device shares one request).
2020
+ this.seedLimiter = createLimiter(MEMBER_SEED_CONCURRENCY);
2021
+ this.seedReads = /* @__PURE__ */ new Map();
2022
+ // bucketMeasurements reads the buckets of every (device, measurement) pair over a window. Widgets
2023
+ // that ask the same question while a read is in flight share it: stat cards on one measurement
2024
+ // and span read on the same minute boundaries, so a board of them costs one read per question.
2025
+ this.bucketReads = /* @__PURE__ */ new Map();
1164
2026
  this.resolver = config.resolver;
1165
2027
  this.authorities = new Set(config.authorities ?? []);
1166
2028
  }
2029
+ // The hub's live-connection state: `live`, `reconnecting` (a socket dropped and is being
2030
+ // retried) or `offline` (down past the point a retry is a blip, or the browser is offline).
2031
+ // A hub with nothing subscribed has nothing to lose and reads `live`. Pair with
2032
+ // onConnectionState (a subscribe/getSnapshot pair, so it drops into useSyncExternalStore).
2033
+ get connectionState() {
2034
+ return this.connection.state;
2035
+ }
2036
+ onConnectionState(listener) {
2037
+ return this.connection.subscribe(listener);
2038
+ }
1167
2039
  // subscribeWidget binds a widget's datasource to a sink and returns a disposer.
1168
2040
  // Selector resolution is async (anchor→devices); the disposer is returned
1169
2041
  // synchronously and cancels a still-pending resolution, so tearing a widget down
@@ -1185,6 +2057,99 @@ var DashboardHub = class {
1185
2057
  });
1186
2058
  return dispose;
1187
2059
  }
2060
+ // subscribeMembers: the roster, then the live samples of every member. Resolution is async, so
2061
+ // the disposer is returned at once and cancels a still-pending resolution (a widget torn down
2062
+ // before its roster arrives never attaches a leaked subscriber). Each member rides the same
2063
+ // ref-counted device stream every other widget on it uses.
2064
+ subscribeMembers(datasource, sink) {
2065
+ let disposed = false;
2066
+ const detachers = [];
2067
+ this.resolveRoster(datasource).then(({ roster, names }) => {
2068
+ if (disposed) return;
2069
+ sink.roster(roster);
2070
+ for (const member of roster.members) {
2071
+ detachers.push(
2072
+ this.attach(member.token, names, { next: (sample) => sink.next(sample), error: (err) => sink.error?.(err) })
2073
+ );
2074
+ }
2075
+ }).catch((err) => {
2076
+ if (disposed) return;
2077
+ if (isForbiddenError(err)) sink.forbidden?.();
2078
+ else sink.error?.(err);
2079
+ });
2080
+ return () => {
2081
+ disposed = true;
2082
+ for (const detach of detachers.splice(0)) detach();
2083
+ };
2084
+ }
2085
+ // One resolution of a selector into a roster. An anchor goes through the resolver's member read
2086
+ // when it has one (names and the true size); everything else is the tokens resolveDevices yields.
2087
+ async resolveRoster(datasource) {
2088
+ const names = new Set(datasource.measurements);
2089
+ if (datasource.kind === "anchor" && this.resolver.anchorMembers) {
2090
+ const { members: members2, total } = await this.resolver.anchorMembers(datasource.anchor);
2091
+ return { roster: { members: members2, total, unbound: false }, names };
2092
+ }
2093
+ const groups = await this.resolveDevices(datasource);
2094
+ const members = groups.map((g) => ({ token: g.deviceToken, name: null }));
2095
+ return { roster: { members, total: members.length, unbound: datasource.kind === "unbound" }, names };
2096
+ }
2097
+ async readMemberSeeds(deviceTokens, plan, onSeed, cancelled = () => false) {
2098
+ await Promise.all(
2099
+ deviceTokens.map(async (token) => {
2100
+ let seed;
2101
+ try {
2102
+ seed = await plan.seed(token, (criteria) => this.memberBuckets(token, criteria, cancelled));
2103
+ } catch (err) {
2104
+ if (cancelled() || err === SEED_CANCELLED) return;
2105
+ onSeed(token, null, isForbiddenError(err) ? "forbidden" : "failed");
2106
+ return;
2107
+ }
2108
+ if (!cancelled()) onSeed(token, seed);
2109
+ })
2110
+ );
2111
+ }
2112
+ async memberBuckets(token, criteria, cancelled) {
2113
+ const key = JSON.stringify([token, criteria]);
2114
+ for (; ; ) {
2115
+ let read = this.seedReads.get(key);
2116
+ if (!read) {
2117
+ read = this.seedLimiter(
2118
+ () => gql2(EVENT_AREA, BUCKETED_MEASUREMENTS, { criteria: { deviceToken: token, ...criteria } }).then((r) => r.bucketedMeasurements),
2119
+ cancelled
2120
+ );
2121
+ this.seedReads.set(key, read);
2122
+ const forget = () => this.seedReads.delete(key);
2123
+ read.then(forget, forget);
2124
+ }
2125
+ try {
2126
+ return await read;
2127
+ } catch (err) {
2128
+ if (err === SEED_CANCELLED && !cancelled()) continue;
2129
+ throw err;
2130
+ }
2131
+ }
2132
+ }
2133
+ bucketMeasurements(datasource, window2) {
2134
+ const key = JSON.stringify([datasource, window2]);
2135
+ let read = this.bucketReads.get(key);
2136
+ if (!read) {
2137
+ read = this.readBuckets(datasource, window2);
2138
+ this.bucketReads.set(key, read);
2139
+ const forget = () => this.bucketReads.delete(key);
2140
+ read.then(forget, forget);
2141
+ }
2142
+ return read;
2143
+ }
2144
+ async readBuckets(datasource, window2) {
2145
+ const groups = await this.resolveDevices(datasource);
2146
+ const reads = groups.flatMap(
2147
+ (g) => (g.names.size ? [...g.names] : [void 0]).map(
2148
+ (name) => gql2(EVENT_AREA, BUCKETED_MEASUREMENTS, { criteria: { deviceToken: g.deviceToken, name, ...window2 } })
2149
+ )
2150
+ );
2151
+ return (await Promise.all(reads)).flatMap((r) => r.bucketedMeasurements);
2152
+ }
1188
2153
  // subscribeAlarms binds an alarm widget's scope+filters to a sink and returns a
1189
2154
  // disposer. Unlike the measurement channel it is NOT multiplexed — alarm widgets are
1190
2155
  // few, and each carries its own filter — so every subscription opens its own trigger
@@ -1227,13 +2192,20 @@ var DashboardHub = class {
1227
2192
  if (debounce) clearTimeout(debounce);
1228
2193
  debounce = setTimeout(() => reconcile(scope.tokens, scope.tenantWide), ALARM_RECONCILE_DEBOUNCE_MS);
1229
2194
  };
2195
+ const handle = this.connection.register();
1230
2196
  const adapter = {
1231
2197
  next: () => trigger(),
1232
2198
  connected: (wasRetry) => {
2199
+ handle.connected();
1233
2200
  if (wasRetry) reconcile(scope.tokens, scope.tenantWide);
1234
- }
2201
+ },
2202
+ closed: () => handle.closed()
2203
+ };
2204
+ const closeSocket = subscribe(DEVICE_AREA, ALARM_STREAM, {}, adapter);
2205
+ unsubscribe = () => {
2206
+ handle.release();
2207
+ closeSocket();
1235
2208
  };
1236
- unsubscribe = subscribe(DEVICE_AREA, ALARM_STREAM, {}, adapter);
1237
2209
  poll = setInterval(() => reconcile(scope.tokens, scope.tenantWide), ALARM_POLL_MS);
1238
2210
  reconciler = () => reconcile(scope.tokens, scope.tenantWide);
1239
2211
  this.alarmReconcilers.add(reconciler);
@@ -1483,6 +2455,8 @@ var DashboardHub = class {
1483
2455
  disposeAll() {
1484
2456
  for (const stream of this.streams.values()) stream.unsubscribe();
1485
2457
  this.streams.clear();
2458
+ for (const stream of this.deadStreams) stream.unsubscribe();
2459
+ this.deadStreams.clear();
1486
2460
  for (const dispose of [...this.alarmDisposers]) dispose();
1487
2461
  this.alarmDisposers.clear();
1488
2462
  for (const dispose of [...this.commandDisposers]) dispose();
@@ -1566,9 +2540,10 @@ var DashboardHub = class {
1566
2540
  stream.subscribers.add(subscriber);
1567
2541
  return () => {
1568
2542
  if (!stream.subscribers.delete(subscriber)) return;
1569
- if (stream.subscribers.size === 0 && this.streams.get(deviceToken) === stream) {
2543
+ if (stream.subscribers.size === 0) {
1570
2544
  stream.unsubscribe();
1571
- this.streams.delete(deviceToken);
2545
+ this.deadStreams.delete(stream);
2546
+ if (this.streams.get(deviceToken) === stream) this.streams.delete(deviceToken);
1572
2547
  }
1573
2548
  };
1574
2549
  }
@@ -1578,16 +2553,63 @@ var DashboardHub = class {
1578
2553
  const stream = { subscribers: /* @__PURE__ */ new Set(), unsubscribe: () => {
1579
2554
  } };
1580
2555
  this.streams.set(deviceToken, stream);
2556
+ const handle = this.connection.register();
2557
+ let closeSocket;
2558
+ let operationOpen = true;
2559
+ let ended = 0;
2560
+ let up = true;
2561
+ let reopen;
2562
+ const closeOperation = () => {
2563
+ if (!operationOpen) return;
2564
+ operationOpen = false;
2565
+ clearTimeout(reopen);
2566
+ closeSocket?.();
2567
+ };
2568
+ const open = () => {
2569
+ closeSocket = subscribe(EVENT_AREA, MEASUREMENT_STREAM, variables, adapter);
2570
+ };
1581
2571
  const adapter = {
1582
- next: (data) => this.fanout(deviceToken, data.measurementStream),
2572
+ next: (data) => {
2573
+ ended = 0;
2574
+ this.fanout(deviceToken, data.measurementStream);
2575
+ },
2576
+ connected: () => {
2577
+ up = true;
2578
+ handle.connected();
2579
+ },
2580
+ closed: () => {
2581
+ up = false;
2582
+ handle.closed();
2583
+ },
2584
+ // 🔴 The server ended this operation on a socket that is still up: its live source
2585
+ // closed, or could not be opened (a resolver error reaches the client as an error-only
2586
+ // `next`, which carries no data, and then this). Left alone, the stream would sit here
2587
+ // receiving nothing while the board read `live` -- a widget frozen until someone reloads,
2588
+ // one stream at a time. Open it again, after a doubling wait capped at 30 s, for as long
2589
+ // as anything shows it. Two other completes are not the server's and are never re-opened:
2590
+ // a widget's own unsubscribe (closeOperation has marked it), and the client being disposed
2591
+ // on sign-out, which closes the socket first (re-opening would bring back the socket the
2592
+ // sign-out exists to drop).
2593
+ complete: () => {
2594
+ if (!operationOpen || !up) return;
2595
+ closeSocket?.();
2596
+ reopen = setTimeout(open, Math.min(1e3 * 2 ** ended++, 3e4));
2597
+ },
1583
2598
  error: (err) => {
1584
2599
  if (this.streams.get(deviceToken) === stream) this.streams.delete(deviceToken);
1585
- stream.unsubscribe();
2600
+ closeOperation();
2601
+ if (stream.subscribers.size > 0) this.deadStreams.add(stream);
2602
+ else handle.release();
1586
2603
  for (const subscriber of stream.subscribers) subscriber.sink.error?.(err);
1587
2604
  }
1588
2605
  };
1589
2606
  const variables = { deviceToken, name: null };
1590
- stream.unsubscribe = subscribe(EVENT_AREA, MEASUREMENT_STREAM, variables, adapter);
2607
+ open();
2608
+ if (!operationOpen) closeSocket?.();
2609
+ stream.unsubscribe = () => {
2610
+ handle.release();
2611
+ closeOperation();
2612
+ };
1591
2613
  return stream;
1592
2614
  }
1593
2615
  fanout(deviceToken, sample) {
@@ -1602,11 +2624,7 @@ var DashboardHub = class {
1602
2624
  };
1603
2625
 
1604
2626
  // src/synthetic.ts
1605
- var SYNTHETIC_GENERATORS = [
1606
- { value: "sine", label: "Sine wave" },
1607
- { value: "ramp", label: "Ramp" },
1608
- { value: "random-walk", label: "Random walk" }
1609
- ];
2627
+ var SYNTHETIC_GENERATORS = ["sine", "ramp", "random-walk"];
1610
2628
  var DEFAULT_NAME = "value";
1611
2629
  var SYNTHETIC_ALARMS = [
1612
2630
  { severity: "CRITICAL", state: "ACTIVE", acknowledged: false, alarmKey: "over-temperature", metricKey: "temperature", lastValue: 87.4, originatorToken: "thermostat-01" },
@@ -1825,8 +2843,71 @@ var SyntheticDataSource = class {
1825
2843
  }
1826
2844
  async clearAlarm() {
1827
2845
  }
1828
- async sendCommand() {
1829
- return { status: "sent", token: "syn-dispatch" };
2846
+ async sendCommand() {
2847
+ return { status: "sent", token: "syn-dispatch" };
2848
+ }
2849
+ // bucketMeasurements answers with ONE bucket per measurement spanning the window, folded from a
2850
+ // synthetic reading per interval, so a preview stat reads something the waveform actually did
2851
+ // (a minimum and maximum inside [min, max]).
2852
+ async bucketMeasurements(datasource, window2) {
2853
+ const names = datasource.measurements.length > 0 ? datasource.measurements : [DEFAULT_NAME];
2854
+ const from = Date.parse(window2.startTime);
2855
+ const to = Date.parse(window2.endTime);
2856
+ return names.map((name) => {
2857
+ const walk = /* @__PURE__ */ new Map();
2858
+ const values = Array.from({ length: 120 }, (_, i) => this.valueFor(name, from + (to - from) * i / 119, walk));
2859
+ const sum = values.reduce((a, v) => a + v, 0);
2860
+ return {
2861
+ bucketStart: window2.startTime,
2862
+ name,
2863
+ avg: sum / values.length,
2864
+ min: Math.min(...values),
2865
+ max: Math.max(...values),
2866
+ sum,
2867
+ count: values.length
2868
+ };
2869
+ });
2870
+ }
2871
+ // A preview fleet: a handful of fabricated devices, each out of phase with the others (the
2872
+ // series is keyed by device and name together), and one that never reports its second
2873
+ // measurement, so a table previews its "does not report this" cell. An unbound slot has no
2874
+ // members, as in the live hub.
2875
+ previewMembers(datasource) {
2876
+ if (datasource.kind === "unbound") return [];
2877
+ return Array.from({ length: 5 }, (_, i) => ({ token: `synthetic-${i + 1}`, name: `Unit ${i + 1}` }));
2878
+ }
2879
+ subscribeMembers(datasource, sink) {
2880
+ const members = this.previewMembers(datasource);
2881
+ const names = datasource.measurements.length > 0 ? datasource.measurements : [DEFAULT_NAME];
2882
+ const walk = /* @__PURE__ */ new Map();
2883
+ let seq = 0;
2884
+ const emit = (token, tMs) => {
2885
+ for (const name of token === "synthetic-4" ? names.slice(0, 1) : names) {
2886
+ sink.next({
2887
+ id: `syn-m-${seq++}`,
2888
+ deviceToken: token,
2889
+ eventType: 0,
2890
+ occurredTime: new Date(tMs).toISOString(),
2891
+ name,
2892
+ value: this.valueFor(`${token}/${name}`, tMs, walk),
2893
+ classifier: null
2894
+ });
2895
+ }
2896
+ };
2897
+ sink.roster({ members, total: members.length, unbound: datasource.kind === "unbound" });
2898
+ for (const m of members) emit(m.token, Date.now());
2899
+ const timer = setInterval(() => {
2900
+ for (const m of members) emit(m.token, Date.now());
2901
+ }, this.intervalMs);
2902
+ this.timers.add(timer);
2903
+ return () => {
2904
+ if (this.timers.delete(timer)) clearInterval(timer);
2905
+ };
2906
+ }
2907
+ // A preview has no history to read: its members report the moment they are subscribed, so the
2908
+ // seed settles empty and the live feed alone fills the cells.
2909
+ async readMemberSeeds(deviceTokens, _plan, onSeed) {
2910
+ for (const token of deviceTokens) onSeed(token, { latest: {}, observed: [] });
1830
2911
  }
1831
2912
  // disposeAll stops every live stream (e.g. when preview is turned off). Individual
1832
2913
  // widget disposers already clear their own timer; this is the belt-and-braces
@@ -1907,7 +2988,7 @@ function bool(v, path) {
1907
2988
  if (typeof v !== "boolean") throw new RecordingFormatError(path, "expected a boolean");
1908
2989
  return v;
1909
2990
  }
1910
- function num(v, path) {
2991
+ function num2(v, path) {
1911
2992
  if (typeof v !== "number" || !Number.isFinite(v)) throw new RecordingFormatError(path, "expected a finite number");
1912
2993
  return v;
1913
2994
  }
@@ -1915,7 +2996,7 @@ function arr(v, path) {
1915
2996
  if (!Array.isArray(v)) throw new RecordingFormatError(path, "expected an array");
1916
2997
  return v;
1917
2998
  }
1918
- function oneOf(v, path, allowed) {
2999
+ function oneOf2(v, path, allowed) {
1919
3000
  if (typeof v !== "string" || !allowed.includes(v)) {
1920
3001
  throw new RecordingFormatError(path, `expected one of ${allowed.join(", ")}`);
1921
3002
  }
@@ -1961,7 +3042,7 @@ function column(v, path, item, length) {
1961
3042
  return a.map((x, i) => item(x, `${path}[${i}]`));
1962
3043
  }
1963
3044
  function nonNegative(v, path) {
1964
- const n = num(v, path);
3045
+ const n = num2(v, path);
1965
3046
  if (n < 0) throw new RecordingFormatError(path, "must not be negative");
1966
3047
  return n;
1967
3048
  }
@@ -2001,7 +3082,7 @@ function parseBoardRecording(input) {
2001
3082
  `unsupported version ${JSON.stringify(root.formatVersion)}; this reader understands ${RECORDING_FORMAT_VERSION}`
2002
3083
  );
2003
3084
  }
2004
- const durationMs = num(root.durationMs, "$.durationMs");
3085
+ const durationMs = num2(root.durationMs, "$.durationMs");
2005
3086
  if (durationMs <= 0) throw new RecordingFormatError("$.durationMs", "must be positive");
2006
3087
  const b = strict(root.build, "$.build", ["gitSha", "sdkCommit"]);
2007
3088
  const build = {
@@ -2010,7 +3091,7 @@ function parseBoardRecording(input) {
2010
3091
  };
2011
3092
  const clock = column(root.clock, "$.clock", (x, p) => {
2012
3093
  const o = strict(x, p, ["fromMs", "mode", "scale"]);
2013
- const scale = num(o.scale, `${p}.scale`);
3094
+ const scale = num2(o.scale, `${p}.scale`);
2014
3095
  if (scale <= 0) throw new RecordingFormatError(`${p}.scale`, "must be positive");
2015
3096
  return { fromMs: nonNegative(o.fromMs, `${p}.fromMs`), mode: str(o.mode, `${p}.mode`), scale };
2016
3097
  });
@@ -2055,7 +3136,7 @@ function parseBoardRecording(input) {
2055
3136
  sourceHashes[key] = matching(sh[key], `$.sourceHashes.${key}`, HEX64, "a lowercase hex SHA-256");
2056
3137
  }
2057
3138
  const cv = strict(root.converter, "$.converter", ["version"]);
2058
- const converter = { version: num(cv.version, "$.converter.version") };
3139
+ const converter = { version: num2(cv.version, "$.converter.version") };
2059
3140
  let excerpt;
2060
3141
  if (root.excerpt !== void 0) {
2061
3142
  const e = strict(root.excerpt, "$.excerpt", ["fromMs", "toMs"]);
@@ -2068,7 +3149,7 @@ function parseBoardRecording(input) {
2068
3149
  const hi = excerpt ? excerpt.toMs : durationMs;
2069
3150
  const chapters = column(root.chapters, "$.chapters", (x, p) => {
2070
3151
  const o = strict(x, p, ["tMs", "name", "note"]);
2071
- const tMs = num(o.tMs, `${p}.tMs`);
3152
+ const tMs = num2(o.tMs, `${p}.tMs`);
2072
3153
  if (tMs < 0 || tMs > durationMs) throw new RecordingFormatError(`${p}.tMs`, "is outside the run");
2073
3154
  if (excerpt && (tMs < lo || tMs > hi)) throw new RecordingFormatError(`${p}.tMs`, "is outside the excerpt window");
2074
3155
  return { tMs, name: str(o.name, `${p}.name`), note: str(o.note, `${p}.note`) };
@@ -2077,7 +3158,7 @@ function parseBoardRecording(input) {
2077
3158
  if (i > 0 && c.tMs < chapters[i - 1].tMs) throw new RecordingFormatError(`$.chapters[${i}].tMs`, "chapters must not decrease in time");
2078
3159
  });
2079
3160
  const m = strict(root.measurements, "$.measurements", ["d", "n", "t", "v", "s"]);
2080
- const t = column(m.t, "$.measurements.t", num);
3161
+ const t = column(m.t, "$.measurements.t", num2);
2081
3162
  let prev = lo;
2082
3163
  t.forEach((x, i) => {
2083
3164
  if (x < lo || x > hi) throw new RecordingFormatError(`$.measurements.t[${i}]`, `${x} is outside [${lo}, ${hi}]`);
@@ -2085,7 +3166,7 @@ function parseBoardRecording(input) {
2085
3166
  prev = x;
2086
3167
  });
2087
3168
  const index = (name, limit) => (x, p) => {
2088
- const n = num(x, p);
3169
+ const n = num2(x, p);
2089
3170
  if (!Number.isInteger(n) || n < 0 || n >= limit) throw new RecordingFormatError(p, `${name} index out of range`);
2090
3171
  return n;
2091
3172
  };
@@ -2093,7 +3174,7 @@ function parseBoardRecording(input) {
2093
3174
  d: column(m.d, "$.measurements.d", index("device", devices.length), t.length),
2094
3175
  n: column(m.n, "$.measurements.n", index("measurement", channels.measurements.length), t.length),
2095
3176
  t,
2096
- v: column(m.v, "$.measurements.v", num, t.length),
3177
+ v: column(m.v, "$.measurements.v", num2, t.length),
2097
3178
  s: column(
2098
3179
  m.s,
2099
3180
  "$.measurements.s",
@@ -2115,8 +3196,8 @@ function parseBoardRecording(input) {
2115
3196
  dev,
2116
3197
  key: str(o.key, `${p}.key`),
2117
3198
  metric: str(o.metric, `${p}.metric`),
2118
- state: oneOf(o.state, `${p}.state`, ALARM_STATES),
2119
- sev: oneOf(o.sev, `${p}.sev`, ALARM_SEVERITIES),
3199
+ state: oneOf2(o.state, `${p}.state`, ALARM_STATES),
3200
+ sev: oneOf2(o.sev, `${p}.sev`, ALARM_SEVERITIES),
2120
3201
  occ: instant(o.occ, `${p}.occ`)
2121
3202
  };
2122
3203
  if (o.ack === true) out.ack = true;
@@ -2139,7 +3220,7 @@ function parseBoardRecording(input) {
2139
3220
  if (total !== alarms.length) {
2140
3221
  throw new RecordingFormatError(`${p}.total`, `is ${total} but ${alarms.length} alarms are listed (truncated snapshot)`);
2141
3222
  }
2142
- const tMs = checkTime(num(o.tMs, `${p}.tMs`), `${p}.tMs`);
3223
+ const tMs = checkTime(num2(o.tMs, `${p}.tMs`), `${p}.tMs`);
2143
3224
  const requestedAtMs = nonNegative(o.requestedAtMs, `${p}.requestedAtMs`);
2144
3225
  if (!Number.isInteger(requestedAtMs)) throw new RecordingFormatError(`${p}.requestedAtMs`, "expected an integer");
2145
3226
  if (requestedAtMs > tMs) throw new RecordingFormatError(`${p}.requestedAtMs`, "a query cannot be sent after its answer");
@@ -2147,7 +3228,7 @@ function parseBoardRecording(input) {
2147
3228
  });
2148
3229
  const events = column(al.events, "$.alarms.events", (x, p) => {
2149
3230
  const o = strict(x, p, [...ALARM_KEYS, "tMs"], ["ack"]);
2150
- return { ...alarm(o, p), tMs: checkTime(num(o.tMs, `${p}.tMs`), `${p}.tMs`) };
3231
+ return { ...alarm(o, p), tMs: checkTime(num2(o.tMs, `${p}.tMs`), `${p}.tMs`) };
2151
3232
  });
2152
3233
  for (const [name, list] of [["snapshots", snapshots], ["events", events]]) {
2153
3234
  list.forEach((e, i) => {
@@ -2442,6 +3523,11 @@ var RecordedDataSource = class {
2442
3523
  get isStale() {
2443
3524
  return this.stale;
2444
3525
  }
3526
+ // The recorded instant the view is at: samples are dated by the run, so a time span is measured
3527
+ // from here, not from the wall clock.
3528
+ now() {
3529
+ return this.startMs + this.cursor;
3530
+ }
2445
3531
  onClock(ev) {
2446
3532
  if (this.stale) return;
2447
3533
  if (ev.reason === "seek") {
@@ -2571,6 +3657,62 @@ var RecordedDataSource = class {
2571
3657
  advance: flush
2572
3658
  });
2573
3659
  }
3660
+ // ── fleets ────────────────────────────────────────────────────────────────
3661
+ // A fleet widget's roster (the run's devices) and then the same back-filled, ticking rows
3662
+ // subscribeWidget delivers, each carrying its device.
3663
+ //
3664
+ // 🔴 A member has NO NAME here, so the widget labels it by its token, exactly as the live board
3665
+ // does. A recorded device's `id` is its external id (the scene's binding key, "SP-HL-0003"),
3666
+ // not a device name: the live roster reads device NAMES, which the sim never sets, and every
3667
+ // other surface of the board (a gauge's subject line, the machine selector, an alarm's
3668
+ // originator) names a device by its token. Labelling the table's rows by external id made one
3669
+ // machine "SP-HL-0003" in the table and "sp-hauler-03" in the alarm beside it.
3670
+ subscribeMembers(datasource, sink) {
3671
+ let members;
3672
+ try {
3673
+ members = this.resolve(datasource).map((g) => ({ token: g.deviceToken, name: null }));
3674
+ } catch (err) {
3675
+ return this.failLater(err, sink);
3676
+ }
3677
+ let stop;
3678
+ const off = this.open({
3679
+ start: () => {
3680
+ sink.roster({ members, total: members.length, unbound: datasource.kind === "unbound" });
3681
+ stop = this.subscribeWidget(datasource, sink);
3682
+ },
3683
+ advance: () => {
3684
+ }
3685
+ });
3686
+ return () => {
3687
+ off();
3688
+ stop?.();
3689
+ };
3690
+ }
3691
+ // Each device's seed comes from the recording itself, with no read to make: its newest reading per
3692
+ // measurement inside the history window, and every measurement it reported in that window, which
3693
+ // is how "does not report this" (a dozer's tyres) is told from "sent nothing".
3694
+ async readMemberSeeds(deviceTokens, _plan, onSeed) {
3695
+ const { t, v } = this.rec.measurements;
3696
+ for (const token of deviceTokens) {
3697
+ const seed = { latest: {}, observed: [] };
3698
+ for (const s of this.index.byDevice.get(token) ?? []) {
3699
+ const at = upperBound(s.times, this.cursor) - 1;
3700
+ if (at < 0 || s.times[at] < this.cursor - RECORDED_HISTORY_WINDOW_MS) continue;
3701
+ const row = s.rows[at];
3702
+ seed.observed.push(s.name);
3703
+ seed.latest[s.name] = {
3704
+ id: `${token}|${s.name}|${row}`,
3705
+ deviceToken: token,
3706
+ eventType: 0,
3707
+ occurredTime: this.iso(t[row]),
3708
+ name: s.name,
3709
+ value: v[row],
3710
+ classifier: null
3711
+ };
3712
+ }
3713
+ onSeed(token, seed);
3714
+ }
3715
+ }
2574
3716
  // ── alarms ────────────────────────────────────────────────────────────────
2575
3717
  subscribeAlarms(subscription, sink) {
2576
3718
  let scope = null;
@@ -2705,28 +3847,269 @@ function toAlarmRow(a) {
2705
3847
  };
2706
3848
  }
2707
3849
 
3850
+ // src/widget-descriptor.ts
3851
+ var BUILTIN_IDS = new Set(WIDGET_TYPES);
3852
+ function isBuiltinWidgetId(id) {
3853
+ return typeof id === "string" && BUILTIN_IDS.has(id);
3854
+ }
3855
+ var NAMESPACED_ID = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
3856
+ function isNamespacedWidgetId(id) {
3857
+ return typeof id === "string" && NAMESPACED_ID.test(id);
3858
+ }
3859
+ function pinnedAxes(mode) {
3860
+ switch (mode) {
3861
+ case "horizontal":
3862
+ return ["rows"];
3863
+ case "vertical":
3864
+ return ["cols"];
3865
+ case "none":
3866
+ return ["cols", "rows"];
3867
+ default:
3868
+ return [];
3869
+ }
3870
+ }
3871
+ var FALLBACK_DEFAULT_SPAN = Object.freeze({ cols: 4, rows: 2 });
3872
+ var SDK_API_VERSION = 1;
3873
+ function defineWidgetDescriptor(descriptor) {
3874
+ return descriptor;
3875
+ }
3876
+
3877
+ // src/layout-lint.ts
3878
+ var PUBLISH_BLOCKING_LAYOUT_CODES = [
3879
+ "span-out-of-limits",
3880
+ "overrun-columns",
3881
+ "relative-off-canvas"
3882
+ ];
3883
+ function blockingLayoutFindings(findings) {
3884
+ return findings.filter((f) => PUBLISH_BLOCKING_LAYOUT_CODES.includes(f.code));
3885
+ }
3886
+ function spanLimitsOfLayout(layout) {
3887
+ if (!layout) return void 0;
3888
+ const min = { ...layout.minSpan };
3889
+ const max = { ...layout.maxSpan };
3890
+ for (const axis of pinnedAxes(layout.resize)) {
3891
+ min[axis] = layout.defaultSpan[axis];
3892
+ max[axis] = layout.defaultSpan[axis];
3893
+ }
3894
+ const limits = {};
3895
+ if (Object.keys(min).length > 0) limits.minSpan = min;
3896
+ if (Object.keys(max).length > 0) limits.maxSpan = max;
3897
+ return limits;
3898
+ }
3899
+ var RELATIVE_PROBE_CELLS = (minCellSize) => [minCellSize, minCellSize * 16];
3900
+ var EDGE_EPSILON = 0.01;
3901
+ function lintLayout(def, options) {
3902
+ const findings = [];
3903
+ const columnsAt = (bp) => bp === BASE_BREAKPOINT || breakpointColumns(def.canvas, bp) === def.canvas.grid.columns ? options.columns : breakpointColumns(def.canvas, bp);
3904
+ const breakpoints = layoutBreakpoints(def.canvas.breakpoints, def.widgets);
3905
+ for (const widget of def.widgets) {
3906
+ const limits = options.spanLimitsOf(widget);
3907
+ if (!limits) {
3908
+ findings.push({
3909
+ code: "no-layout-declared",
3910
+ severity: "info",
3911
+ widgetId: widget.id,
3912
+ message: `${widget.type} declares no span limits, so its size cannot be checked`
3913
+ });
3914
+ }
3915
+ for (const [bp, placement] of Object.entries(widget.layout)) {
3916
+ const where = bp === BASE_BREAKPOINT ? "" : ` at breakpoint ${bp}`;
3917
+ const at = bp === BASE_BREAKPOINT ? {} : { breakpoint: bp };
3918
+ const columns = columnsAt(bp);
3919
+ if (placement.mode === "grid" && placement.col + placement.cols > columns) {
3920
+ findings.push({
3921
+ code: "overrun-columns",
3922
+ severity: "error",
3923
+ widgetId: widget.id,
3924
+ ...at,
3925
+ message: `spans columns ${placement.col}..${placement.col + placement.cols} of ${columns}${where}`
3926
+ });
3927
+ }
3928
+ const span = spanOf(def, widget, bp, placement);
3929
+ if (limits && span) {
3930
+ const problem = spanProblem(span.cols, span.rows, limits);
3931
+ if (problem) {
3932
+ findings.push({
3933
+ code: "span-out-of-limits",
3934
+ severity: "error",
3935
+ widgetId: widget.id,
3936
+ ...at,
3937
+ message: `${span.cols}x${span.rows} ${problem}${where}`
3938
+ });
3939
+ }
3940
+ }
3941
+ }
3942
+ const duplicate = titleDuplicatesSubtitle(def, widget);
3943
+ if (duplicate) {
3944
+ findings.push({
3945
+ code: "title-duplicates-subtitle",
3946
+ severity: "warning",
3947
+ widgetId: widget.id,
3948
+ message: duplicate
3949
+ });
3950
+ }
3951
+ }
3952
+ const byId = new Map(def.widgets.map((w) => [w.id, w]));
3953
+ const differsFromBase = (w, bp) => {
3954
+ if (columnsAt(bp) !== options.columns) return true;
3955
+ for (let cur = w, hops = 0; cur && hops < 3; hops++) {
3956
+ if (Object.prototype.hasOwnProperty.call(cur.layout, bp)) return true;
3957
+ const p = resolvePlacement(cur.layout, bp);
3958
+ cur = p.mode === "relative" ? byId.get(p.host) : void 0;
3959
+ }
3960
+ return false;
3961
+ };
3962
+ for (const bp of breakpoints) {
3963
+ const columns = columnsAt(bp);
3964
+ const gutter = gutterPx(def.canvas.grid);
3965
+ const off = /* @__PURE__ */ new Set();
3966
+ for (const cell of RELATIVE_PROBE_CELLS(def.canvas.grid.minCellSize)) {
3967
+ const width = spanPx(columns, cell, gutter.x);
3968
+ for (const [id, r] of layoutRects(def.widgets, bp, cell, gutter)) {
3969
+ const w = byId.get(id);
3970
+ if (resolvePlacement(w.layout, bp).mode !== "relative" || off.has(id)) continue;
3971
+ if (bp !== BASE_BREAKPOINT && !differsFromBase(w, bp)) continue;
3972
+ if (r.x < -EDGE_EPSILON || r.y < -EDGE_EPSILON || r.x + r.w > width + EDGE_EPSILON) {
3973
+ off.add(id);
3974
+ const where = bp === BASE_BREAKPOINT ? "" : ` at breakpoint ${bp}`;
3975
+ findings.push({
3976
+ code: "relative-off-canvas",
3977
+ severity: "error",
3978
+ widgetId: id,
3979
+ ...bp === BASE_BREAKPOINT ? {} : { breakpoint: bp },
3980
+ message: `falls outside the ${columns}-column canvas${where} (left ${round(r.x)}, top ${round(r.y)}, right ${round(r.x + r.w)} of ${round(width)} px at ${round(cell)} px cells)`
3981
+ });
3982
+ }
3983
+ }
3984
+ }
3985
+ }
3986
+ return findings;
3987
+ }
3988
+ function round(n) {
3989
+ return Math.round(n * 10) / 10;
3990
+ }
3991
+ function spanOf(def, widget, bp, p) {
3992
+ if (p.mode === "grid") return { cols: p.cols, rows: p.rows };
3993
+ if (!("fill" in p.size)) return p.size;
3994
+ const host = def.widgets.find((w) => w.id === p.host);
3995
+ if (!host || host === widget) return void 0;
3996
+ return spanOf(def, host, bp, resolvePlacement(host.layout, bp));
3997
+ }
3998
+ function spanProblem(cols, rows, limits) {
3999
+ const { minSpan = {}, maxSpan = {} } = limits;
4000
+ const bound = (b) => b.cols === void 0 ? `${b.rows}-row` : b.rows === void 0 ? `${b.cols}-column` : `${b.cols}x${b.rows}`;
4001
+ if (cols < (minSpan.cols ?? 1) || rows < (minSpan.rows ?? 1)) {
4002
+ return `is below the ${bound(minSpan)} minimum`;
4003
+ }
4004
+ if (cols > (maxSpan.cols ?? Infinity) || rows > (maxSpan.rows ?? Infinity)) {
4005
+ return `is above the ${bound(maxSpan)} maximum`;
4006
+ }
4007
+ return void 0;
4008
+ }
4009
+ function titleDuplicatesSubtitle(def, widget) {
4010
+ const title = widget.options?.title;
4011
+ if (typeof title !== "string" || title.trim() === "") return void 0;
4012
+ const binding = widgetBinding(def, widget);
4013
+ const subject = binding?.kind === "device" ? binding.deviceToken : binding?.anchor.targetToken;
4014
+ if (!subject) return void 0;
4015
+ const ds = widget.datasource;
4016
+ const slotLabel = ds?.kind === "slot" ? def.slots?.[ds.slot]?.label : void 0;
4017
+ const same = (a) => sameLabel(a, title);
4018
+ if (same(subject)) return `the title and the subject line both read "${title.trim()}"`;
4019
+ if (same(slotLabel)) {
4020
+ return `the title "${title.trim()}" is the slot's name for "${subject}", which the subject line already shows`;
4021
+ }
4022
+ return void 0;
4023
+ }
4024
+
2708
4025
  // src/editor-model.ts
2709
- function baseBox(widget) {
4026
+ function basePlacement(widget) {
2710
4027
  return widget.layout[BASE_BREAKPOINT];
2711
4028
  }
2712
- function setWidgetBox(def, id, box) {
2713
- return {
4029
+ function resolveCollisions(def, breakpoint, first, aside = false) {
4030
+ const placed = [];
4031
+ for (const w of def.widgets) {
4032
+ const p = resolvePlacement(w.layout, breakpoint);
4033
+ if (p.mode === "grid") placed.push({ id: w.id, placement: p });
4034
+ }
4035
+ const resolved = aside ? pushAside(placed, first) : pushDown(placed, first);
4036
+ let changed = false;
4037
+ const widgets = def.widgets.map((w) => {
4038
+ const before = resolvePlacement(w.layout, breakpoint);
4039
+ const after = resolved.get(w.id);
4040
+ if (!after || after === before) return w;
4041
+ changed = true;
4042
+ return { ...w, layout: { ...w.layout, [breakpoint]: after } };
4043
+ });
4044
+ return changed ? { ...def, widgets } : def;
4045
+ }
4046
+ function resolveEverywhere(def, first, aside = false) {
4047
+ let next = def;
4048
+ for (const bp of layoutBreakpoints(def.canvas.breakpoints, def.widgets))
4049
+ next = resolveCollisions(next, bp, first, aside && bp === BASE_BREAKPOINT);
4050
+ return next;
4051
+ }
4052
+ function setWidgetPlacement(def, id, placement, options = {}) {
4053
+ const next = {
2714
4054
  ...def,
2715
- widgets: def.widgets.map(
2716
- (w) => w.id === id ? { ...w, layout: { ...w.layout, [BASE_BREAKPOINT]: box } } : w
2717
- )
4055
+ widgets: def.widgets.map((w) => w.id === id ? { ...w, layout: { ...w.layout, [BASE_BREAKPOINT]: placement } } : w)
2718
4056
  };
4057
+ return placement.mode === "grid" ? resolveEverywhere(next, [id], options.nearest) : next;
4058
+ }
4059
+ function clampSpan(span, limits, room) {
4060
+ const cols = Math.min(room, Math.max(limits?.minSpan?.cols ?? 1, Math.min(limits?.maxSpan?.cols ?? Infinity, span.cols)));
4061
+ const rows = Math.max(limits?.minSpan?.rows ?? 1, Math.min(limits?.maxSpan?.rows ?? Infinity, span.rows));
4062
+ return { cols: Math.max(1, Math.round(cols)), rows: Math.max(1, Math.round(rows)) };
4063
+ }
4064
+ function moveWidget(def, id, to, columns = def.canvas.grid.columns, options = {}) {
4065
+ const w = def.widgets.find((x) => x.id === id);
4066
+ if (!w) return def;
4067
+ const p = basePlacement(w);
4068
+ if (p.mode !== "grid") return def;
4069
+ const col = Math.min(Math.max(0, Math.round(to.col)), Math.max(0, columns - p.cols));
4070
+ const row = Math.max(0, Math.round(to.row));
4071
+ if (col === p.col && row === p.row) return def;
4072
+ return setWidgetPlacement(def, id, { ...p, col, row }, options);
4073
+ }
4074
+ function resizeWidget(def, id, target, limits, edges = {}, columns = def.canvas.grid.columns, options = {}) {
4075
+ const w = def.widgets.find((x) => x.id === id);
4076
+ if (!w) return def;
4077
+ const p = basePlacement(w);
4078
+ if (p.mode !== "grid") return def;
4079
+ const right = target.col + target.cols;
4080
+ const bottom = target.row + target.rows;
4081
+ const room = edges.fromLeft ? Math.max(1, right) : Math.max(1, columns - Math.max(0, target.col));
4082
+ const { cols, rows } = clampSpan(target, limits, Math.min(columns, room));
4083
+ const col = edges.fromLeft ? Math.max(0, right - cols) : Math.max(0, target.col);
4084
+ const row = edges.fromTop ? Math.max(0, bottom - rows) : Math.max(0, target.row);
4085
+ if (col === p.col && row === p.row && cols === p.cols && rows === p.rows) return def;
4086
+ return setWidgetPlacement(def, id, { mode: "grid", col, row, cols, rows }, options);
2719
4087
  }
2720
4088
  function deleteWidget(def, id) {
2721
- return { ...def, widgets: def.widgets.filter((w) => w.id !== id) };
4089
+ const gone = def.widgets.find((w) => w.id === id);
4090
+ let next = { ...def, widgets: def.widgets.filter((w) => w.id !== id) };
4091
+ if (!gone) return next;
4092
+ for (const bp of layoutBreakpoints(def.canvas.breakpoints, def.widgets)) {
4093
+ const hostPlacement = resolvePlacement(gone.layout, bp);
4094
+ const columns = breakpointColumns(def.canvas, bp);
4095
+ for (const w of next.widgets) {
4096
+ const p = resolvePlacement(w.layout, bp);
4097
+ if (p.mode !== "relative" || p.host !== id) continue;
4098
+ const size = "fill" in p.size ? hostSpan(hostPlacement, next, bp) : p.size;
4099
+ const occupied = next.widgets.map((o) => resolvePlacement(o.layout, bp)).filter((o) => o.mode === "grid");
4100
+ const cols = Math.min(size.cols, columns);
4101
+ const slot = firstFreeSlot(occupied, cols, size.rows, columns);
4102
+ const grid = { mode: "grid", col: slot.col, row: slot.row, cols, rows: size.rows };
4103
+ next = { ...next, widgets: next.widgets.map((o) => o.id === w.id ? { ...o, layout: { ...o.layout, [bp]: grid } } : o) };
4104
+ }
4105
+ }
4106
+ return resolveEverywhere(next, []);
2722
4107
  }
2723
- function bringToFront(def, id) {
2724
- const widget = def.widgets.find((w) => w.id === id);
2725
- if (!widget) return def;
2726
- const box = baseBox(widget);
2727
- const maxOther = Math.max(-Infinity, ...def.widgets.filter((w) => w.id !== id).map((w) => baseBox(w).z));
2728
- if (box.z > maxOther) return def;
2729
- return setWidgetBox(def, id, { ...box, z: maxOther + 1 });
4108
+ function hostSpan(p, def, bp) {
4109
+ if (p.mode === "grid") return { cols: p.cols, rows: p.rows };
4110
+ if (!("fill" in p.size)) return p.size;
4111
+ const host = def.widgets.find((w) => w.id === p.host);
4112
+ return host ? hostSpan(resolvePlacement(host.layout, bp), def, bp) : { cols: 1, rows: 1 };
2730
4113
  }
2731
4114
  function setTitle(def, title) {
2732
4115
  return { ...def, title };
@@ -2734,28 +4117,44 @@ function setTitle(def, title) {
2734
4117
  function setCanvasGrid(def, patch) {
2735
4118
  const grid = { ...def.canvas.grid, ...patch };
2736
4119
  if (patch.columns !== void 0) grid.columns = Math.max(1, Math.round(patch.columns));
2737
- if (patch.rowHeight !== void 0) grid.rowHeight = Math.max(1, Math.round(patch.rowHeight));
2738
- const widgets = grid.columns < def.canvas.grid.columns ? def.widgets.map((w) => clampWidgetColumns(w, grid.columns)) : def.widgets;
2739
- return { ...def, widgets, canvas: { ...def.canvas, grid } };
2740
- }
2741
- function clampWidgetColumns(widget, columns) {
2742
- const layout = {};
2743
- for (const [bp, box] of Object.entries(widget.layout)) {
2744
- const col = Math.min(box.col, columns - 1);
2745
- layout[bp] = { ...box, col, colSpan: Math.min(box.colSpan, columns - col) };
4120
+ if (patch.minCellSize !== void 0) grid.minCellSize = Math.max(1, patch.minCellSize);
4121
+ let next = { ...def, canvas: { ...def.canvas, grid } };
4122
+ if (grid.columns >= def.canvas.grid.columns) return next;
4123
+ const affected = layoutBreakpoints(def.canvas.breakpoints, def.widgets).filter(
4124
+ (bp) => breakpointColumns(next.canvas, bp) === grid.columns
4125
+ );
4126
+ for (const bp of affected) {
4127
+ next = {
4128
+ ...next,
4129
+ widgets: next.widgets.map((w) => {
4130
+ const p = resolvePlacement(w.layout, bp);
4131
+ if (p.mode !== "grid" || p.col + p.cols <= grid.columns) return w;
4132
+ const col = Math.min(p.col, grid.columns - 1);
4133
+ return { ...w, layout: { ...w.layout, [bp]: { ...p, col, cols: Math.min(p.cols, grid.columns - col) } } };
4134
+ })
4135
+ };
4136
+ next = resolveCollisions(next, bp, []);
2746
4137
  }
2747
- return { ...widget, layout };
4138
+ return next;
2748
4139
  }
2749
4140
  function setCanvasSizing(def, sizing) {
2750
4141
  return { ...def, canvas: { ...def.canvas, sizing } };
2751
4142
  }
4143
+ function setCanvasDensity(def, density) {
4144
+ const { density: _prev, ...rest } = def.canvas;
4145
+ return { ...def, canvas: density === DEFAULT_CANVAS_DENSITY ? rest : { ...rest, density } };
4146
+ }
2752
4147
  function updateWidget(def, id, next) {
2753
4148
  return { ...def, widgets: def.widgets.map((w) => w.id === id ? next : w) };
2754
4149
  }
2755
4150
  function addWidget(def, descriptor) {
2756
- const maxZ = def.widgets.reduce((m, w) => Math.max(m, baseBox(w).z), 0);
2757
4151
  const id = generateWidgetId();
2758
- const box = { col: 0, colSpan: 8, row: 0, rowSpan: 4, z: maxZ + 1 };
4152
+ const span = descriptor.layout?.defaultSpan ?? FALLBACK_DEFAULT_SPAN;
4153
+ const columns = def.canvas.grid.columns;
4154
+ const cols = Math.min(span.cols, columns);
4155
+ const occupied = def.widgets.map(basePlacement).filter((p) => p.mode === "grid");
4156
+ const slot = firstFreeSlot(occupied, cols, span.rows, columns);
4157
+ const placement = { mode: "grid", col: slot.col, row: slot.row, cols, rows: span.rows };
2759
4158
  const common = {
2760
4159
  id,
2761
4160
  widget: {
@@ -2763,44 +4162,414 @@ function addWidget(def, descriptor) {
2763
4162
  optionsVersion: descriptor.optionsVersion,
2764
4163
  sdkApiVersion: descriptor.sdkApiVersion
2765
4164
  },
2766
- layout: { [BASE_BREAKPOINT]: box },
4165
+ layout: { [BASE_BREAKPOINT]: placement },
2767
4166
  options: { ...descriptor.defaults }
2768
4167
  };
2769
4168
  const widget = { ...common, type: descriptor.id };
2770
- return { definition: { ...def, widgets: [...def.widgets, widget] }, id };
4169
+ return { definition: resolveEverywhere({ ...def, widgets: [...def.widgets, widget] }, [id]), id };
2771
4170
  }
2772
- function gridBoxToPx(box, geom) {
2773
- const colStride = geom.colWidth + geom.colGap;
2774
- const rowStride = geom.rowHeight + geom.rowGap;
2775
- const dx = box.offset?.x ?? 0;
2776
- const dy = box.offset?.y ?? 0;
4171
+ function gridPlacementToPx(p, geom) {
2777
4172
  return {
2778
- x: box.col * colStride + dx,
2779
- y: box.row * rowStride + dy,
2780
- w: box.colSpan * geom.colWidth + (box.colSpan - 1) * geom.colGap,
2781
- h: box.rowSpan * geom.rowHeight + (box.rowSpan - 1) * geom.rowGap
4173
+ x: p.col * (geom.cell + geom.gutter.x),
4174
+ y: p.row * (geom.cell + geom.gutter.y),
4175
+ w: spanPx(p.cols, geom.cell, geom.gutter.x),
4176
+ h: spanPx(p.rows, geom.cell, geom.gutter.y)
2782
4177
  };
2783
4178
  }
2784
- function pxToGridBox(px, geom, z, offset, columns) {
2785
- const colStride = Math.max(1, geom.colWidth + geom.colGap);
2786
- const rowStride = Math.max(1, geom.rowHeight + geom.rowGap);
2787
- const x = px.x - (offset?.x ?? 0);
2788
- const y = px.y - (offset?.y ?? 0);
2789
- let col = Math.max(0, Math.round(x / colStride));
2790
- let colSpan = Math.max(1, Math.round((px.w + geom.colGap) / colStride));
4179
+ function pxToGridPlacement(px, geom, columns) {
4180
+ const colStride = Math.max(1, geom.cell + geom.gutter.x);
4181
+ const rowStride = Math.max(1, geom.cell + geom.gutter.y);
4182
+ let col = Math.max(0, Math.round(px.x / colStride));
4183
+ let cols = Math.max(1, Math.round((px.w + geom.gutter.x) / colStride));
2791
4184
  if (columns !== void 0) {
2792
4185
  col = Math.min(col, columns - 1);
2793
- colSpan = Math.min(colSpan, columns - col);
4186
+ cols = Math.min(cols, columns - col);
2794
4187
  }
2795
- const box = {
4188
+ return {
4189
+ mode: "grid",
2796
4190
  col,
2797
- colSpan,
2798
- row: Math.max(0, Math.round(y / rowStride)),
2799
- rowSpan: Math.max(1, Math.round((px.h + geom.rowGap) / rowStride)),
2800
- z
4191
+ row: Math.max(0, Math.round(px.y / rowStride)),
4192
+ cols,
4193
+ rows: Math.max(1, Math.round((px.h + geom.gutter.y) / rowStride))
4194
+ };
4195
+ }
4196
+ function formatSpan(span) {
4197
+ return `${span.cols}\xD7${span.rows}`;
4198
+ }
4199
+ var OFFSET_STEP = 1;
4200
+ var OFFSET_STEP_FINE = 0.25;
4201
+ function snapOffset(value, fine = false) {
4202
+ const step = fine ? OFFSET_STEP_FINE : OFFSET_STEP;
4203
+ const snapped = Math.round(value / step) * step;
4204
+ return snapped === 0 ? 0 : snapped;
4205
+ }
4206
+ function replaceBase(def, id, placement) {
4207
+ return {
4208
+ ...def,
4209
+ widgets: def.widgets.map((w) => w.id === id ? { ...w, layout: { ...w.layout, [BASE_BREAKPOINT]: placement } } : w)
4210
+ };
4211
+ }
4212
+ function setRelativeOffset(def, id, offset, options = {}) {
4213
+ const w = def.widgets.find((x2) => x2.id === id);
4214
+ if (!w) return def;
4215
+ const p = basePlacement(w);
4216
+ if (p.mode !== "relative") return def;
4217
+ const step = options.fine ? OFFSET_STEP_FINE : OFFSET_STEP;
4218
+ let x = snapOffset(offset.x, options.fine);
4219
+ let y = snapOffset(offset.y, options.fine);
4220
+ const { bounds } = options;
4221
+ if (bounds) {
4222
+ const host = layoutRects(def.widgets, BASE_BREAKPOINT, bounds.cell, bounds.gutter).get(p.host);
4223
+ if (host) {
4224
+ const r = relativeRect({ ...p, offset: { x, y } }, host, bounds.cell, bounds.gutter);
4225
+ const px = (bounds.cell + bounds.gutter.x) * step;
4226
+ const py = (bounds.cell + bounds.gutter.y) * step;
4227
+ const width = spanPx(bounds.columns, bounds.cell, bounds.gutter.x);
4228
+ if (r.x < 0) x += Math.ceil(-r.x / px) * step;
4229
+ else if (r.x + r.w > width) x -= Math.ceil((r.x + r.w - width) / px) * step;
4230
+ if (r.y < 0) y += Math.ceil(-r.y / py) * step;
4231
+ }
4232
+ }
4233
+ const next = { ...p };
4234
+ if (x === 0 && y === 0) delete next.offset;
4235
+ else next.offset = { x, y };
4236
+ if ((next.offset?.x ?? 0) === (p.offset?.x ?? 0) && (next.offset?.y ?? 0) === (p.offset?.y ?? 0)) return def;
4237
+ return replaceBase(def, id, next);
4238
+ }
4239
+ function canHost(def, host, id) {
4240
+ if (!host || host.id === id) return false;
4241
+ return layoutBreakpoints(def.canvas.breakpoints, def.widgets).every(
4242
+ (bp) => resolvePlacement(host.layout, bp).mode === "grid"
4243
+ );
4244
+ }
4245
+ function canHostRelative(def, id, hostId) {
4246
+ return canHost(def, def.widgets.find((x) => x.id === hostId), id);
4247
+ }
4248
+ function placeRelative(def, id, hostId, anchor) {
4249
+ const w = def.widgets.find((x) => x.id === id);
4250
+ if (!w) return def;
4251
+ if (!canHost(def, def.widgets.find((x) => x.id === hostId), id)) return def;
4252
+ const p = basePlacement(w);
4253
+ const size = p.mode === "grid" ? { cols: p.cols, rows: p.rows } : p.size;
4254
+ return replaceBase(def, id, { mode: "relative", host: hostId, anchor, size });
4255
+ }
4256
+ function setRelativeAnchor(def, id, anchor) {
4257
+ const w = def.widgets.find((x) => x.id === id);
4258
+ const p = w && basePlacement(w);
4259
+ if (!p || p.mode !== "relative" || p.anchor === anchor) return def;
4260
+ return replaceBase(def, id, { ...p, anchor });
4261
+ }
4262
+ function placeOnGrid(def, id) {
4263
+ const w = def.widgets.find((x) => x.id === id);
4264
+ if (!w) return def;
4265
+ const p = basePlacement(w);
4266
+ if (p.mode !== "relative") return def;
4267
+ const columns = def.canvas.grid.columns;
4268
+ const size = "fill" in p.size ? hostSpan(p, def, BASE_BREAKPOINT) : p.size;
4269
+ const cols = Math.min(size.cols, columns);
4270
+ const occupied = def.widgets.filter((o) => o.id !== id).map(basePlacement).filter((o) => o.mode === "grid");
4271
+ const slot = firstFreeSlot(occupied, cols, size.rows, columns);
4272
+ const grid = { mode: "grid", col: slot.col, row: slot.row, cols, rows: size.rows };
4273
+ return resolveEverywhere(replaceBase(def, id, grid), [id]);
4274
+ }
4275
+ function fitToLimits(def, spanLimitsOf) {
4276
+ let changed = false;
4277
+ const columnsAt = (bp) => breakpointColumns(def.canvas, bp);
4278
+ const clamped = {
4279
+ ...def,
4280
+ widgets: def.widgets.map((w) => {
4281
+ const limits = spanLimitsOf(w);
4282
+ const layout = {};
4283
+ let touched = false;
4284
+ for (const [bp, p] of Object.entries(w.layout)) {
4285
+ const columns = columnsAt(bp);
4286
+ let q = p;
4287
+ if (p.mode === "grid") {
4288
+ const span = clampSpan(p, limits, columns);
4289
+ const col = Math.max(0, Math.min(p.col, columns - span.cols));
4290
+ if (span.cols !== p.cols || span.rows !== p.rows || col !== p.col) q = { ...p, ...span, col };
4291
+ } else if (!("fill" in p.size)) {
4292
+ const size = clampSpan(p.size, limits, columns);
4293
+ if (size.cols !== p.size.cols || size.rows !== p.size.rows) q = { ...p, size };
4294
+ }
4295
+ if (q !== p) touched = true;
4296
+ layout[bp] = q;
4297
+ }
4298
+ if (!touched) return w;
4299
+ changed = true;
4300
+ return { ...w, layout };
4301
+ })
4302
+ };
4303
+ let next = {
4304
+ ...clamped,
4305
+ widgets: clamped.widgets.map((w) => {
4306
+ const limits = spanLimitsOf(w);
4307
+ if (!limits) return w;
4308
+ let layout;
4309
+ for (const [bp, p] of Object.entries(w.layout)) {
4310
+ if (p.mode !== "relative" || !("fill" in p.size)) continue;
4311
+ const span = hostSpan(p, clamped, bp);
4312
+ const size = clampSpan(span, limits, columnsAt(bp));
4313
+ if (size.cols === span.cols && size.rows === span.rows) continue;
4314
+ layout = { ...layout ?? w.layout, [bp]: { ...p, size } };
4315
+ }
4316
+ if (!layout) return w;
4317
+ changed = true;
4318
+ return { ...w, layout };
4319
+ })
4320
+ };
4321
+ for (let pass = 0; pass < 3; pass++) {
4322
+ const off = lintLayout(next, { columns: def.canvas.grid.columns, spanLimitsOf }).filter(
4323
+ (f) => f.code === "relative-off-canvas"
4324
+ );
4325
+ if (off.length === 0) break;
4326
+ for (const f of off) {
4327
+ const fixed = fitRelativeOnCanvas(next, f.widgetId, f.breakpoint ?? BASE_BREAKPOINT);
4328
+ if (fixed !== next) changed = true;
4329
+ next = fixed;
4330
+ }
4331
+ }
4332
+ if (!changed) return def;
4333
+ return resolveEverywhere(next, []);
4334
+ }
4335
+ function fitRelativeOnCanvas(def, id, bp) {
4336
+ const w = def.widgets.find((x) => x.id === id);
4337
+ if (!w) return def;
4338
+ const p = resolvePlacement(w.layout, bp);
4339
+ if (p.mode !== "relative") return def;
4340
+ const columns = breakpointColumns(def.canvas, bp);
4341
+ const gutter = gutterPx(def.canvas.grid);
4342
+ const { offset, ...bare } = p;
4343
+ let loX = -Infinity;
4344
+ let hiX = Infinity;
4345
+ let loY = -Infinity;
4346
+ for (const cell of RELATIVE_PROBE_CELLS(def.canvas.grid.minCellSize)) {
4347
+ const host = layoutRects(def.widgets, bp, cell, gutter).get(p.host);
4348
+ if (!host) return def;
4349
+ const r = relativeRect(bare, host, cell, gutter);
4350
+ const width = spanPx(columns, cell, gutter.x);
4351
+ loX = Math.max(loX, -r.x / (cell + gutter.x));
4352
+ hiX = Math.min(hiX, (width - r.x - r.w) / (cell + gutter.x));
4353
+ loY = Math.max(loY, -r.y / (cell + gutter.y));
4354
+ }
4355
+ const step = OFFSET_STEP_FINE;
4356
+ const up = (v) => Math.ceil(v / step - 1e-9) * step;
4357
+ const down = (v) => Math.floor(v / step + 1e-9) * step;
4358
+ let placement;
4359
+ if (up(loX) <= down(hiX)) {
4360
+ const x = Math.min(Math.max(offset?.x ?? 0, up(loX)), down(hiX)) || 0;
4361
+ const y = Math.max(offset?.y ?? 0, up(loY)) || 0;
4362
+ placement = x === 0 && y === 0 ? bare : { ...bare, offset: { x, y } };
4363
+ } else {
4364
+ const size = "fill" in p.size ? hostSpan(p, def, bp) : p.size;
4365
+ const cols = Math.min(size.cols, columns);
4366
+ const occupied = def.widgets.filter((o) => o.id !== id).map((o) => resolvePlacement(o.layout, bp)).filter((o) => o.mode === "grid");
4367
+ const slot = firstFreeSlot(occupied, cols, size.rows, columns);
4368
+ placement = { mode: "grid", col: slot.col, row: slot.row, cols, rows: size.rows };
4369
+ }
4370
+ return {
4371
+ ...def,
4372
+ widgets: def.widgets.map((x) => x.id === id ? { ...x, layout: { ...x.layout, [bp]: placement } } : x)
4373
+ };
4374
+ }
4375
+
4376
+ // src/editor-history.ts
4377
+ var DEFAULT_HISTORY_LIMIT = 100;
4378
+ function createEditHistory(present, options = {}) {
4379
+ return { past: [], present, future: [], limit: Math.max(1, Math.floor(options.limit ?? DEFAULT_HISTORY_LIMIT)) };
4380
+ }
4381
+ function commitEdit(h, next, label, options = {}) {
4382
+ const equals = options.equals ?? Object.is;
4383
+ if (next === h.present || equals(next, h.present)) return h;
4384
+ const last = h.past[h.past.length - 1];
4385
+ if (options.key !== void 0 && options.key === h.open && last) {
4386
+ if (equals(next, last.state)) return { past: h.past.slice(0, -1), present: next, future: [], limit: h.limit };
4387
+ return { ...h, present: next, future: [] };
4388
+ }
4389
+ const past = [...h.past, { state: h.present, label }];
4390
+ if (past.length > h.limit) past.splice(0, past.length - h.limit);
4391
+ return { past, present: next, future: [], open: options.key, limit: h.limit };
4392
+ }
4393
+ function sealHistory(h) {
4394
+ if (h.open === void 0) return h;
4395
+ const { open: _closed, ...rest } = h;
4396
+ return rest;
4397
+ }
4398
+ function undoEdit(h) {
4399
+ const last = h.past[h.past.length - 1];
4400
+ if (!last) return { history: h };
4401
+ return {
4402
+ history: {
4403
+ past: h.past.slice(0, -1),
4404
+ present: last.state,
4405
+ future: [...h.future, { state: h.present, label: last.label }],
4406
+ limit: h.limit
4407
+ },
4408
+ label: last.label
4409
+ };
4410
+ }
4411
+ function redoEdit(h) {
4412
+ const next = h.future[h.future.length - 1];
4413
+ if (!next) return { history: h };
4414
+ return {
4415
+ history: {
4416
+ past: [...h.past, { state: h.present, label: next.label }],
4417
+ present: next.state,
4418
+ future: h.future.slice(0, -1),
4419
+ limit: h.limit
4420
+ },
4421
+ label: next.label
4422
+ };
4423
+ }
4424
+ var canUndo = (h) => h.past.length > 0;
4425
+ var canRedo = (h) => h.future.length > 0;
4426
+ function rebaseHistory(h, fn) {
4427
+ const map = (e) => ({ ...e, state: fn(e.state) });
4428
+ return { ...h, past: h.past.map(map), present: fn(h.present), future: h.future.map(map) };
4429
+ }
4430
+
4431
+ // src/editor-clipboard.ts
4432
+ var WIDGET_CLIPBOARD_FORMAT = "devicechain/dashboard-widgets";
4433
+ var WIDGET_CLIPBOARD_VERSION = 1;
4434
+ function slotReferences(w) {
4435
+ const out = [];
4436
+ if (w.datasource?.kind === "slot" && w.datasource.slot) out.push(w.datasource.slot);
4437
+ const target = w.options?.selectionTarget;
4438
+ if (typeof target === "string" && target !== "" && !out.includes(target)) out.push(target);
4439
+ return out;
4440
+ }
4441
+ var own = (map, key) => map && Object.prototype.hasOwnProperty.call(map, key) ? map[key] : void 0;
4442
+ function copyWidget(def, id, origin) {
4443
+ const w = def.widgets.find((x) => x.id === id);
4444
+ if (!w || w.type === UNKNOWN_WIDGET_TYPE) return void 0;
4445
+ const p = basePlacement(w);
4446
+ const size = p.mode === "grid" ? { cols: p.cols, rows: p.rows } : "fill" in p.size ? hostSpan(p, def, BASE_BREAKPOINT) : p.size;
4447
+ const placement = { mode: "grid", col: 0, row: 0, ...size };
4448
+ const widget = structuredClone({ ...w, layout: { [BASE_BREAKPOINT]: placement } });
4449
+ const slots = {};
4450
+ for (const name of slotReferences(w)) {
4451
+ const slot = own(def.slots, name);
4452
+ if (slot) slots[name] = structuredClone(slot);
4453
+ }
4454
+ return { tenant: origin.tenant, board: origin.board, canvas: structuredClone(def.canvas), widgets: [widget], slots };
4455
+ }
4456
+ function serializeWidgetClipboard(clip) {
4457
+ const definition = storedDefinition({
4458
+ schemaVersion: CURRENT_SCHEMA_VERSION,
4459
+ title: "",
4460
+ canvas: clip.canvas,
4461
+ widgets: clip.widgets,
4462
+ ...Object.keys(clip.slots).length > 0 ? { slots: clip.slots } : {}
4463
+ });
4464
+ return JSON.stringify({
4465
+ format: WIDGET_CLIPBOARD_FORMAT,
4466
+ version: WIDGET_CLIPBOARD_VERSION,
4467
+ tenant: clip.tenant,
4468
+ board: clip.board,
4469
+ definition
4470
+ });
4471
+ }
4472
+ function parseWidgetClipboard(text, options = {}) {
4473
+ let env;
4474
+ try {
4475
+ env = JSON.parse(text);
4476
+ } catch {
4477
+ return void 0;
4478
+ }
4479
+ if (typeof env !== "object" || env === null) return void 0;
4480
+ const { format, version, tenant, board, definition } = env;
4481
+ if (format !== WIDGET_CLIPBOARD_FORMAT || version !== WIDGET_CLIPBOARD_VERSION) return void 0;
4482
+ if (typeof tenant !== "string" || tenant === "" || typeof board !== "string") return void 0;
4483
+ let parsed;
4484
+ try {
4485
+ parsed = parseDashboardDefinition(definition, options);
4486
+ } catch (err) {
4487
+ if (err?.code === "schemaTooNew") throw err;
4488
+ return void 0;
4489
+ }
4490
+ const widgets = [];
4491
+ const refused = [];
4492
+ for (const w of parsed.widgets) {
4493
+ if (w.type !== UNKNOWN_WIDGET_TYPE) {
4494
+ widgets.push(w);
4495
+ continue;
4496
+ }
4497
+ const type = typeof w.raw.type === "string" ? w.raw.type : "";
4498
+ refused.push({ type, reason: w.reason?.message ?? "" });
4499
+ }
4500
+ return { clipboard: { tenant, board, canvas: parsed.canvas, widgets, slots: parsed.slots ?? {} }, refused };
4501
+ }
4502
+ var CONCRETE_KINDS = /* @__PURE__ */ new Set(["device", "anchor", "devices", "relatedTraversal"]);
4503
+ function pasteWidgets(def, clip, at) {
4504
+ const sameTenant = clip.tenant !== "" && clip.tenant === at.tenant;
4505
+ const sameBoard = sameTenant && clip.board === at.board;
4506
+ const columns = def.canvas.grid.columns;
4507
+ const slots = { ...def.slots ?? {} };
4508
+ const mapped = /* @__PURE__ */ new Map();
4509
+ const mapSlot = (from) => {
4510
+ const known = mapped.get(from);
4511
+ if (known) return known;
4512
+ const source = own(clip.slots, from);
4513
+ const target = own(slots, from);
4514
+ const keep = sameTenant && target !== void 0 && source !== void 0 && target.type === source.type && (sameBoard || sameBinding(target.defaultBinding, source.defaultBinding));
4515
+ let result;
4516
+ if (keep) result = { slot: from };
4517
+ else {
4518
+ const slot = nextSlotName(slots);
4519
+ const label = sameTenant ? source?.label : void 0;
4520
+ slots[slot] = { type: source?.type ?? "device", ...label !== void 0 ? { label } : {} };
4521
+ result = { slot, reason: !sameTenant ? "tenant" : target ? "different" : "missing" };
4522
+ }
4523
+ mapped.set(from, result);
4524
+ return result;
2801
4525
  };
2802
- if (offset) box.offset = offset;
2803
- return box;
4526
+ const occupied = def.widgets.map(basePlacement).filter((p) => p.mode === "grid");
4527
+ const added = [];
4528
+ const ids = [];
4529
+ const unbound = [];
4530
+ for (const source of clip.widgets) {
4531
+ const id = generateWidgetId();
4532
+ const p = basePlacement(source);
4533
+ const span = p.mode === "grid" ? { cols: p.cols, rows: p.rows } : "fill" in p.size ? { cols: 1, rows: 1 } : p.size;
4534
+ const cols = Math.min(span.cols, columns);
4535
+ const slot = firstFreeSlot(occupied, cols, span.rows, columns);
4536
+ const placement = { mode: "grid", col: slot.col, row: slot.row, cols, rows: span.rows };
4537
+ occupied.push(placement);
4538
+ const w = structuredClone(source);
4539
+ w.id = id;
4540
+ w.layout = { [BASE_BREAKPOINT]: placement };
4541
+ for (const from of slotReferences(source)) {
4542
+ const m = mapSlot(from);
4543
+ if (m.reason) unbound.push({ widgetId: id, slot: m.slot, from, reason: m.reason });
4544
+ if (m.slot === from) continue;
4545
+ if (w.datasource?.kind === "slot" && w.datasource.slot === from) w.datasource = { ...w.datasource, slot: m.slot };
4546
+ if (w.options?.selectionTarget === from) w.options = { ...w.options, selectionTarget: m.slot };
4547
+ }
4548
+ const ds = w.datasource;
4549
+ if (!sameTenant && ds && CONCRETE_KINDS.has(ds.kind) && ds.kind !== "slot" && ds.kind !== "unbound") {
4550
+ const slot2 = nextSlotName(slots);
4551
+ slots[slot2] = { type: ds.kind === "anchor" ? "anchor" : "device" };
4552
+ w.datasource = {
4553
+ kind: "slot",
4554
+ slot: slot2,
4555
+ measurements: [...ds.measurements],
4556
+ ...ds.location ? { location: { ...ds.location } } : {}
4557
+ };
4558
+ unbound.push({ widgetId: id, slot: slot2, from: "", reason: "tenant" });
4559
+ }
4560
+ added.push(w);
4561
+ ids.push(id);
4562
+ }
4563
+ const next = { ...def, widgets: [...def.widgets, ...added] };
4564
+ if (Object.keys(slots).length > 0) next.slots = slots;
4565
+ return { definition: resolveEverywhere(next, ids), ids, unbound };
4566
+ }
4567
+ function duplicateWidget(def, id) {
4568
+ const here = { tenant: "here", board: "here" };
4569
+ const clip = copyWidget(def, id, here);
4570
+ if (!clip) return void 0;
4571
+ const r = pasteWidgets(def, clip, here);
4572
+ return { definition: r.definition, id: r.ids[0], unbound: r.unbound };
2804
4573
  }
2805
4574
 
2806
4575
  // src/resolver.ts
@@ -2808,28 +4577,63 @@ import { gql as gql3 } from "@devicechain/client";
2808
4577
  var ANCHOR_PAGE_SIZE = 500;
2809
4578
  function createDeviceResolver() {
2810
4579
  const anchorCache = /* @__PURE__ */ new Map();
4580
+ const memberCache = /* @__PURE__ */ new Map();
2811
4581
  const existsCache = /* @__PURE__ */ new Map();
4582
+ function anchorKey(anchor) {
4583
+ return `${anchor.relationship}|${anchor.targetType}|${anchor.targetToken}`;
4584
+ }
4585
+ function anchorPage(anchor) {
4586
+ const key = anchorKey(anchor);
4587
+ let pending = anchorCache.get(key);
4588
+ if (!pending) {
4589
+ pending = gql3("device-management", DEVICES_FOR_ANCHOR, {
4590
+ criteria: {
4591
+ pageNumber: 1,
4592
+ pageSize: ANCHOR_PAGE_SIZE,
4593
+ sourceType: "device",
4594
+ targetType: anchor.targetType,
4595
+ target: anchor.targetToken,
4596
+ relationshipType: anchor.relationship
4597
+ }
4598
+ }).then((r) => {
4599
+ const tokens = r.entityRelationships.results.map((rel) => rel.source.token);
4600
+ const total = r.entityRelationships.pagination?.totalRecords ?? tokens.length;
4601
+ return { tokens, total: Math.max(total, tokens.length) };
4602
+ }).catch((err) => {
4603
+ anchorCache.delete(key);
4604
+ throw err;
4605
+ });
4606
+ anchorCache.set(key, pending);
4607
+ }
4608
+ return pending;
4609
+ }
2812
4610
  return {
2813
4611
  devicesForAnchor(anchor) {
2814
- const key = `${anchor.relationship}|${anchor.targetType}|${anchor.targetToken}`;
2815
- let pending = anchorCache.get(key);
4612
+ return anchorPage(anchor).then((page) => page.tokens);
4613
+ },
4614
+ // The members of an anchor with their names and the anchor's true size. One relationship page
4615
+ // (shared with devicesForAnchor) plus ONE batched name read, whatever the size of the fleet. A
4616
+ // failed name read is not a failed roster: the tokens still stream, and the widget labels
4617
+ // them by token.
4618
+ anchorMembers(anchor) {
4619
+ const key = anchorKey(anchor);
4620
+ let pending = memberCache.get(key);
2816
4621
  if (!pending) {
2817
- pending = gql3("device-management", DEVICES_FOR_ANCHOR, {
2818
- criteria: {
2819
- pageNumber: 1,
2820
- pageSize: ANCHOR_PAGE_SIZE,
2821
- sourceType: "device",
2822
- targetType: anchor.targetType,
2823
- target: anchor.targetToken,
2824
- relationshipType: anchor.relationship
4622
+ pending = anchorPage(anchor).then(async ({ tokens, total }) => {
4623
+ const names = /* @__PURE__ */ new Map();
4624
+ if (tokens.length > 0) {
4625
+ try {
4626
+ const r = await gql3("device-management", DEVICE_NAMES, { tokens });
4627
+ for (const d of r.devicesByToken) names.set(d.token, d.name);
4628
+ } catch {
4629
+ }
2825
4630
  }
2826
- }).then(
2827
- (r) => r.entityRelationships.results.map((rel) => rel.source.token)
2828
- ).catch((err) => {
2829
- anchorCache.delete(key);
4631
+ return { members: tokens.map((token) => ({ token, name: names.get(token) ?? null })), total };
4632
+ }).catch((err) => {
4633
+ memberCache.delete(key);
2830
4634
  throw err;
2831
4635
  });
2832
- anchorCache.set(key, pending);
4636
+ memberCache.set(key, pending);
2833
4637
  }
2834
4638
  return pending;
2835
4639
  },
@@ -2861,15 +4665,14 @@ function createDeviceResolver() {
2861
4665
 
2862
4666
  // src/history.ts
2863
4667
  import { gql as gql4 } from "@devicechain/client";
4668
+ function historyWindowFor(spanSeconds, end) {
4669
+ const { startTime, endTime } = rangeWindow({ spanSeconds, paused: false }, end);
4670
+ return { startTime, endTime, intervalSeconds: bucketSecondsFor(spanSeconds) };
4671
+ }
2864
4672
  function defaultHistoryWindow() {
2865
- const now = Date.now();
2866
- return {
2867
- startTime: new Date(now - 60 * 60 * 1e3).toISOString(),
2868
- endTime: new Date(now).toISOString(),
2869
- intervalSeconds: 60
2870
- };
4673
+ return historyWindowFor(DEFAULT_TIME_RANGE_SECONDS, Date.now());
2871
4674
  }
2872
- async function fetchWidgetHistory(widget, window) {
4675
+ async function fetchWidgetHistory(widget, window2) {
2873
4676
  const ds = widget.datasource;
2874
4677
  if (!ds || ds.kind !== "device") return [];
2875
4678
  try {
@@ -2881,9 +4684,9 @@ async function fetchWidgetHistory(widget, window) {
2881
4684
  criteria: {
2882
4685
  deviceToken,
2883
4686
  name,
2884
- startTime: window.startTime,
2885
- endTime: window.endTime,
2886
- intervalSeconds: window.intervalSeconds
4687
+ startTime: window2.startTime,
4688
+ endTime: window2.endTime,
4689
+ intervalSeconds: window2.intervalSeconds
2887
4690
  }
2888
4691
  }).then((r) => r.bucketedMeasurements)
2889
4692
  )
@@ -2990,6 +4793,36 @@ function checkValue(widgetId, key, spec, value) {
2990
4793
  return issue("enum", `${key} must be one of ${spec.values.join(", ")}, got ${JSON.stringify(value)}`);
2991
4794
  }
2992
4795
  return [];
4796
+ case "list": {
4797
+ if (!Array.isArray(value)) return issue("type", `${key} must be a list, got ${describe(value)}`);
4798
+ if (spec.maxItems !== void 0 && value.length > spec.maxItems) {
4799
+ return issue("range", `${key} holds at most ${spec.maxItems} entries, got ${value.length}`);
4800
+ }
4801
+ const out = [];
4802
+ value.forEach((entry, i) => {
4803
+ const at = `${key}[${i}]`;
4804
+ const at_ = (path, code, message) => ({ widgetId, key: path, code, message });
4805
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
4806
+ out.push(at_(at, "type", `${at} must be an object, got ${describe(entry)}`));
4807
+ return;
4808
+ }
4809
+ const record = entry;
4810
+ for (const [field, fieldSpec] of Object.entries(spec.item)) {
4811
+ const fieldValue = record[field];
4812
+ if (fieldValue === void 0 || fieldSpec.required && fieldValue === "") {
4813
+ if (fieldSpec.required) out.push(at_(`${at}.${field}`, "missing", `${at}.${field} is required but not set`));
4814
+ continue;
4815
+ }
4816
+ out.push(...checkValue(widgetId, `${at}.${field}`, fieldSpec, fieldValue));
4817
+ }
4818
+ for (const field of Object.keys(record)) {
4819
+ if (!hasOwn(spec.item, field)) {
4820
+ out.push(at_(`${at}.${field}`, "unknown", `${at}.${field} is not a field of ${key} entries`));
4821
+ }
4822
+ }
4823
+ });
4824
+ return out;
4825
+ }
2993
4826
  }
2994
4827
  }
2995
4828
  function parsesAsJson(value) {
@@ -3029,7 +4862,7 @@ function applyOptionPatch(specs, options, patch, label = "widget") {
3029
4862
  for (const [key, value] of Object.entries(patch)) {
3030
4863
  if (!hasOwn(specs, key)) throw new Error(`${label}: "${key}" is not a declared option`);
3031
4864
  const spec = specs[key];
3032
- if (value === void 0 || value === "" || spec.kind === "boolean" && value === false) {
4865
+ if (value === void 0 || value === "" || spec.kind === "boolean" && value === false || spec.kind === "list" && Array.isArray(value) && value.length === 0) {
3033
4866
  delete next[key];
3034
4867
  continue;
3035
4868
  }
@@ -3059,25 +4892,17 @@ function applyOptionPatch(specs, options, patch, label = "widget") {
3059
4892
  }
3060
4893
  next[key] = value;
3061
4894
  break;
4895
+ case "list": {
4896
+ const bad = checkValue(label, key, spec, value);
4897
+ if (bad.length > 0) refuse(`a list of ${key} entries (${bad[0].message})`);
4898
+ next[key] = value;
4899
+ break;
4900
+ }
3062
4901
  }
3063
4902
  }
3064
4903
  return next;
3065
4904
  }
3066
4905
 
3067
- // src/widget-descriptor.ts
3068
- var BUILTIN_IDS = new Set(WIDGET_TYPES);
3069
- function isBuiltinWidgetId(id) {
3070
- return typeof id === "string" && BUILTIN_IDS.has(id);
3071
- }
3072
- var NAMESPACED_ID = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
3073
- function isNamespacedWidgetId(id) {
3074
- return typeof id === "string" && NAMESPACED_ID.test(id);
3075
- }
3076
- var SDK_API_VERSION = 1;
3077
- function defineWidgetDescriptor(descriptor) {
3078
- return descriptor;
3079
- }
3080
-
3081
4906
  // src/widget-catalog.ts
3082
4907
  var WidgetCatalogError = class extends Error {
3083
4908
  constructor(issues, subject = "widget catalog") {
@@ -3091,28 +4916,26 @@ function describeIssues(subject, issues) {
3091
4916
  return `invalid ${subject} (${issues.length} problem${issues.length === 1 ? "" : "s"}):
3092
4917
  ${lines.join("\n")}`;
3093
4918
  }
3094
- var CHANNELS = ["measurement", "alarm", "control", "selection", "location", "none"];
4919
+ var CHANNELS = ["measurement", "alarm", "control", "selection", "location", "aggregate", "none"];
3095
4920
  var SEMVER2 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
3096
4921
  var SELECTOR_KINDS = ["device", "anchor", "slot"];
3097
4922
  var BINDING_KEYS = {
3098
- measurement: ["window"],
4923
+ measurement: ["window", "windowSeconds", "members"],
3099
4924
  alarm: ["pageSize", "state", "severity", "acknowledged"],
3100
4925
  control: ["pageSize"],
3101
4926
  location: [],
4927
+ aggregate: ["windowSeconds"],
3102
4928
  selection: [],
3103
4929
  none: []
3104
4930
  };
3105
- function isRecord3(value) {
3106
- return typeof value === "object" && value !== null && !Array.isArray(value);
3107
- }
3108
4931
  function isPlainObject(value) {
3109
- if (!isRecord3(value)) return false;
4932
+ if (!isRecord(value)) return false;
3110
4933
  const proto = Object.getPrototypeOf(value);
3111
4934
  return proto === Object.prototype || proto === null;
3112
4935
  }
3113
4936
  var join = (base, key) => base ? `${base}.${key}` : key;
3114
4937
  function validateWidgetDescriptor(descriptor) {
3115
- const rawId = isRecord3(descriptor) && typeof descriptor.id === "string" ? descriptor.id : "";
4938
+ const rawId = isRecord(descriptor) && typeof descriptor.id === "string" ? descriptor.id : "";
3116
4939
  return collectDescriptorIssues(descriptor, rawId || "#?");
3117
4940
  }
3118
4941
  function collectDescriptorIssues(d, id) {
@@ -3139,7 +4962,7 @@ function collectDescriptorIssues(d, id) {
3139
4962
  if (typeof d.optionsVersion !== "number" || !Number.isInteger(d.optionsVersion) || d.optionsVersion < 1) {
3140
4963
  add("optionsVersion", `optionsVersion must be a positive integer, got ${JSON.stringify(d.optionsVersion)}`);
3141
4964
  }
3142
- if (!isRecord3(d.display)) {
4965
+ if (!isRecord(d.display)) {
3143
4966
  add("display", "display must be an object");
3144
4967
  } else {
3145
4968
  if (typeof d.display.title !== "string" || d.display.title === "") {
@@ -3155,10 +4978,15 @@ function collectDescriptorIssues(d, id) {
3155
4978
  const specs = checkOptions(d.options, add);
3156
4979
  const constraints = checkConstraints(d.constraints, specs, add);
3157
4980
  if (specs) checkDefaults(d.defaults, specs, constraints, typeof d.id === "string" ? d.id : id, add);
3158
- else if (!isRecord3(d.defaults)) add("defaults", "defaults must be an object");
4981
+ else if (!isRecord(d.defaults)) add("defaults", "defaults must be an object");
3159
4982
  checkDatasource(d.datasource, channel, add);
3160
4983
  if (channel) checkBinding(d.binding, channel, specs, add);
3161
4984
  checkInteractions(d.interactions, channel, add);
4985
+ checkLayout(d.layout, add);
4986
+ if (d.freshness !== void 0) {
4987
+ if (d.freshness !== true) add("freshness", "freshness must be true when present");
4988
+ else if (channel && channel !== "measurement") add("freshness", "freshness applies only to the measurement channel");
4989
+ }
3162
4990
  return issues;
3163
4991
  }
3164
4992
  function jsonOnly(value, path, add, ancestors) {
@@ -3197,14 +5025,17 @@ function jsonOnly(value, path, add, ancestors) {
3197
5025
  ancestors.delete(obj);
3198
5026
  }
3199
5027
  function checkOptions(options, add) {
3200
- if (!isRecord3(options)) {
5028
+ if (!isRecord(options)) {
3201
5029
  add("options", "options must be an object mapping option keys to specs");
3202
5030
  return void 0;
3203
5031
  }
5032
+ return checkOptionsAt(options, "options.", add) ? options : void 0;
5033
+ }
5034
+ function checkOptionsAt(options, prefix, add) {
3204
5035
  let sound = true;
3205
5036
  for (const [key, spec] of Object.entries(options)) {
3206
- const at = `options.${key}`;
3207
- if (!isRecord3(spec)) {
5037
+ const at = `${prefix}${key}`;
5038
+ if (!isRecord(spec)) {
3208
5039
  add(at, "an option spec must be an object");
3209
5040
  sound = false;
3210
5041
  continue;
@@ -3235,7 +5066,7 @@ function checkOptions(options, add) {
3235
5066
  for (const table of ["valueLabels", "valueLabelIds"]) {
3236
5067
  const labels = spec[table];
3237
5068
  if (labels === void 0) continue;
3238
- if (!isRecord3(labels)) {
5069
+ if (!isRecord(labels)) {
3239
5070
  add(`${at}.${table}`, `${table} must be an object mapping enum values to strings`);
3240
5071
  continue;
3241
5072
  }
@@ -3247,12 +5078,33 @@ function checkOptions(options, add) {
3247
5078
  }
3248
5079
  }
3249
5080
  break;
5081
+ case "list": {
5082
+ if (spec.maxItems !== void 0 && (typeof spec.maxItems !== "number" || !Number.isInteger(spec.maxItems) || spec.maxItems < 1)) {
5083
+ add(`${at}.maxItems`, "maxItems must be a whole number of at least 1");
5084
+ }
5085
+ if (!isRecord(spec.item) || Object.keys(spec.item).length === 0) {
5086
+ add(`${at}.item`, "a list option needs an item object declaring at least one field");
5087
+ sound = false;
5088
+ break;
5089
+ }
5090
+ const fields = spec.item;
5091
+ const flat = Object.fromEntries(Object.entries(fields).filter(([, f]) => !isRecord(f) || f.kind !== "list"));
5092
+ for (const key2 of Object.keys(fields)) {
5093
+ if (!(key2 in flat)) add(`${at}.item.${key2}`, "a list item field cannot itself be a list");
5094
+ }
5095
+ if (Object.keys(flat).length !== Object.keys(fields).length) {
5096
+ sound = false;
5097
+ break;
5098
+ }
5099
+ if (!checkOptionsAt(flat, `${at}.item.`, add)) sound = false;
5100
+ break;
5101
+ }
3250
5102
  default:
3251
- add(`${at}.kind`, `kind must be string, number, boolean or enum, got ${JSON.stringify(spec.kind)}`);
5103
+ add(`${at}.kind`, `kind must be string, number, boolean, enum or list, got ${JSON.stringify(spec.kind)}`);
3252
5104
  sound = false;
3253
5105
  }
3254
5106
  }
3255
- return sound ? options : void 0;
5107
+ return sound;
3256
5108
  }
3257
5109
  function checkConstraints(constraints, specs, add) {
3258
5110
  if (constraints === void 0) return [];
@@ -3263,8 +5115,8 @@ function checkConstraints(constraints, specs, add) {
3263
5115
  const valid = [];
3264
5116
  constraints.forEach((c, i) => {
3265
5117
  const at = `constraints[${i}]`;
3266
- if (!isRecord3(c) || c.kind !== "greater-than") {
3267
- add(`${at}.kind`, `constraint kind must be "greater-than", got ${JSON.stringify(isRecord3(c) ? c.kind : c)}`);
5118
+ if (!isRecord(c) || c.kind !== "greater-than") {
5119
+ add(`${at}.kind`, `constraint kind must be "greater-than", got ${JSON.stringify(isRecord(c) ? c.kind : c)}`);
3268
5120
  return;
3269
5121
  }
3270
5122
  let ok = true;
@@ -3283,7 +5135,7 @@ function checkConstraints(constraints, specs, add) {
3283
5135
  return valid;
3284
5136
  }
3285
5137
  function checkDefaults(defaults, specs, constraints, id, add) {
3286
- if (!isRecord3(defaults)) {
5138
+ if (!isRecord(defaults)) {
3287
5139
  add("defaults", "defaults must be an object");
3288
5140
  return;
3289
5141
  }
@@ -3293,7 +5145,7 @@ function checkDefaults(defaults, specs, constraints, id, add) {
3293
5145
  for (const issue of found) add(`defaults.${issue.key}`, issue.message);
3294
5146
  }
3295
5147
  function checkDatasource(datasource, channel, add) {
3296
- if (!isRecord3(datasource)) {
5148
+ if (!isRecord(datasource)) {
3297
5149
  add("datasource", "datasource must be an object");
3298
5150
  return;
3299
5151
  }
@@ -3323,7 +5175,7 @@ function checkDatasource(datasource, channel, add) {
3323
5175
  });
3324
5176
  }
3325
5177
  function checkBinding(binding, channel, specs, add) {
3326
- if (!isRecord3(binding)) {
5178
+ if (!isRecord(binding)) {
3327
5179
  add("binding", "binding must be an object");
3328
5180
  return;
3329
5181
  }
@@ -3338,7 +5190,7 @@ function checkBinding(binding, channel, specs, add) {
3338
5190
  const policy = binding[key];
3339
5191
  if (policy === void 0) return;
3340
5192
  const at = `binding.${key}`;
3341
- if (!isRecord3(policy)) {
5193
+ if (!isRecord(policy)) {
3342
5194
  add(at, `${key} must be an object`);
3343
5195
  return;
3344
5196
  }
@@ -3365,7 +5217,7 @@ function checkBinding(binding, channel, specs, add) {
3365
5217
  }
3366
5218
  if (policy.min !== void 0 && typeof policy.min !== "number") add(`${at}.min`, "min must be a number");
3367
5219
  if (policy.default !== void 0) {
3368
- const wantsNumber = key === "window" || key === "pageSize";
5220
+ const wantsNumber = key === "window" || key === "windowSeconds" || key === "pageSize";
3369
5221
  if (typeof policy.default !== (wantsNumber ? "number" : "string")) {
3370
5222
  add(`${at}.default`, `default must be a ${wantsNumber ? "number" : "string"}`);
3371
5223
  }
@@ -3375,13 +5227,83 @@ function checkBinding(binding, channel, specs, add) {
3375
5227
  const isText = (s) => s.kind === "string" || s.kind === "enum";
3376
5228
  const isBoolEnum = (s) => s.kind === "enum" && s.values.every((v) => v === "true" || v === "false");
3377
5229
  ref("window", isNumber, "a number option", false);
5230
+ ref("windowSeconds", isNumber, "a number option", false);
3378
5231
  ref("pageSize", isNumber, "a number option", true);
3379
5232
  ref("state", isText, "a string or enum option", false);
3380
5233
  ref("severity", isText, "a string or enum option", false);
3381
5234
  ref("acknowledged", isBoolEnum, 'an enum option whose values are only "true" and "false"', false);
5235
+ const members = binding.members;
5236
+ if (members !== void 0) {
5237
+ const m = isRecord(members) ? members : {};
5238
+ const isConst = m.const !== void 0;
5239
+ if (isConst ? m.option !== void 0 || m.equals !== void 0 : typeof m.option !== "string" || typeof m.equals !== "string") {
5240
+ add("binding.members", "members must be { const: true } or { option, equals } naming a declared option and a value of it");
5241
+ } else if (isConst) {
5242
+ if (m.const !== true) add("binding.members.const", `const must be true, got ${JSON.stringify(m.const)}`);
5243
+ } else if (specs) {
5244
+ const spec = hasOwn(specs, m.option) ? specs[m.option] : void 0;
5245
+ if (!spec) add("binding.members.option", `option ${JSON.stringify(m.option)} is not a declared option key`);
5246
+ else if (!isText(spec)) add("binding.members.option", `option ${JSON.stringify(m.option)} must be a string or enum option`);
5247
+ else if (spec.kind === "enum" && !spec.values.includes(m.equals)) {
5248
+ add("binding.members.equals", `${JSON.stringify(m.equals)} is not one of the option's values`);
5249
+ }
5250
+ }
5251
+ }
5252
+ }
5253
+ function readSpan(value, path, add) {
5254
+ if (!isRecord(value)) {
5255
+ add(path, `${path} must be an object with cols and rows`);
5256
+ return void 0;
5257
+ }
5258
+ let ok = true;
5259
+ for (const axis of ["cols", "rows"]) {
5260
+ const n = value[axis];
5261
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1) {
5262
+ add(`${path}.${axis}`, `${axis} must be a positive integer, got ${JSON.stringify(n)}`);
5263
+ ok = false;
5264
+ }
5265
+ }
5266
+ for (const key of Object.keys(value)) {
5267
+ if (key !== "cols" && key !== "rows") add(`${path}.${key}`, `unknown span field "${key}"`);
5268
+ }
5269
+ return ok ? { cols: value.cols, rows: value.rows } : void 0;
5270
+ }
5271
+ function checkLayout(layout, add) {
5272
+ if (layout === void 0) return;
5273
+ if (!isRecord(layout)) {
5274
+ add("layout", "layout must be an object");
5275
+ return;
5276
+ }
5277
+ for (const key of Object.keys(layout)) {
5278
+ if (!["defaultSpan", "minSpan", "maxSpan", "resize"].includes(key)) add(`layout.${key}`, `unknown layout field "${key}"`);
5279
+ }
5280
+ let def;
5281
+ if (layout.defaultSpan === void 0) add("layout.defaultSpan", "layout needs a defaultSpan");
5282
+ else def = readSpan(layout.defaultSpan, "layout.defaultSpan", add);
5283
+ const min = layout.minSpan === void 0 ? void 0 : readSpan(layout.minSpan, "layout.minSpan", add);
5284
+ const max = layout.maxSpan === void 0 ? void 0 : readSpan(layout.maxSpan, "layout.maxSpan", add);
5285
+ const mode = layout.resize ?? "both";
5286
+ if (mode !== "both" && mode !== "horizontal" && mode !== "vertical" && mode !== "none") {
5287
+ add("layout.resize", `resize must be both, horizontal, vertical or none, got ${JSON.stringify(layout.resize)}`);
5288
+ return;
5289
+ }
5290
+ const floor = min ?? { cols: 1, rows: 1 };
5291
+ for (const axis of ["cols", "rows"]) {
5292
+ if (def && floor[axis] > def[axis]) add("layout.minSpan", `minSpan.${axis} (${floor[axis]}) is above defaultSpan.${axis} (${def[axis]})`);
5293
+ if (def && max && def[axis] > max[axis]) add("layout.maxSpan", `maxSpan.${axis} (${max[axis]}) is below defaultSpan.${axis} (${def[axis]})`);
5294
+ if (!def && min && max && min[axis] > max[axis]) add("layout.maxSpan", `maxSpan.${axis} is below minSpan.${axis}`);
5295
+ }
5296
+ if (!def) return;
5297
+ for (const axis of pinnedAxes(mode)) {
5298
+ for (const [name, bound] of [["minSpan", min], ["maxSpan", max]]) {
5299
+ if (bound && bound[axis] !== def[axis]) {
5300
+ add("layout.resize", `resize "${mode}" pins ${axis} at ${def[axis]}, but ${name}.${axis} is ${bound[axis]}`);
5301
+ }
5302
+ }
5303
+ }
3382
5304
  }
3383
5305
  function checkInteractions(interactions, channel, add) {
3384
- if (!isRecord3(interactions)) {
5306
+ if (!isRecord(interactions)) {
3385
5307
  add("interactions", "interactions must be an object");
3386
5308
  return;
3387
5309
  }
@@ -3416,7 +5338,7 @@ function collectCatalogIssues(descriptors) {
3416
5338
  const issues = [];
3417
5339
  const seen = /* @__PURE__ */ new Map();
3418
5340
  descriptors.forEach((d, index) => {
3419
- const id = isRecord3(d) && typeof d.id === "string" && d.id !== "" ? d.id : `#${index}`;
5341
+ const id = isRecord(d) && typeof d.id === "string" && d.id !== "" ? d.id : `#${index}`;
3420
5342
  issues.push(...collectDescriptorIssues(d, id));
3421
5343
  if (id.startsWith("#")) return;
3422
5344
  const first = seen.get(id);
@@ -3435,41 +5357,151 @@ function sealCatalog(descriptors) {
3435
5357
  list: () => ordered
3436
5358
  });
3437
5359
  }
5360
+
5361
+ // src/reflow.ts
5362
+ function liveColumns(grid, containerWidth, authored, scale = 1) {
5363
+ const minCell = grid.minCellSize * scale;
5364
+ if (cellSizeAt(grid, containerWidth, authored, scale) >= minCell) return authored;
5365
+ const gx = gutterPx(grid, scale).x;
5366
+ const room = cellSizeAt(grid, containerWidth, 1, scale);
5367
+ const fit = Math.floor((room + gx) / (minCell + gx));
5368
+ return Math.min(authored, Math.max(PHONE_MIN_COLUMNS, fit));
5369
+ }
5370
+ var clampInto = (n, min, max) => Math.min(max ?? Infinity, Math.max(min ?? 1, n));
5371
+ var STACK_BELOW_WIDTH = 600;
5372
+ function reflowGrid(items, columns, options = {}) {
5373
+ const order = items.map((item, index) => ({ item, index })).sort(
5374
+ (a, b) => a.item.placement.row - b.item.placement.row || a.item.placement.col - b.item.placement.col || a.index - b.index
5375
+ );
5376
+ const placed = [];
5377
+ const out = /* @__PURE__ */ new Map();
5378
+ let floor = 0;
5379
+ let bottom = 0;
5380
+ for (const { item } of order) {
5381
+ const { placement: p, limits } = item;
5382
+ const want = options.stack ? columns : p.cols;
5383
+ const cols = Math.min(columns, clampInto(want, limits?.minSpan?.cols, limits?.maxSpan?.cols));
5384
+ const rows = clampInto(p.rows, limits?.minSpan?.rows, limits?.maxSpan?.rows);
5385
+ if (options.stack) {
5386
+ const spot2 = { mode: "grid", col: 0, row: bottom, cols, rows };
5387
+ out.set(item.id, spot2);
5388
+ bottom += rows;
5389
+ continue;
5390
+ }
5391
+ let spot;
5392
+ for (let row = floor; !spot; row++) {
5393
+ for (let col = 0; col + cols <= columns; col++) {
5394
+ const probe = { mode: "grid", col, row, cols, rows };
5395
+ if (!placed.some((q) => gridFootprintsOverlap(probe, q))) {
5396
+ spot = probe;
5397
+ break;
5398
+ }
5399
+ }
5400
+ }
5401
+ placed.push(spot);
5402
+ out.set(item.id, spot);
5403
+ floor = spot.row;
5404
+ }
5405
+ return out;
5406
+ }
5407
+ function layoutShape(canvas, containerWidth, scale = 1) {
5408
+ const measured = containerWidth > 0;
5409
+ const breakpoint = measured ? activeBreakpoint(canvas.breakpoints, containerWidth) : BASE_BREAKPOINT;
5410
+ const authored = breakpointColumns(canvas, breakpoint);
5411
+ const columns = measured ? liveColumns(canvas.grid, containerWidth, authored, scale) : authored;
5412
+ const reflowed = columns < authored;
5413
+ return { breakpoint, columns, reflowed, stacked: reflowed && containerWidth < STACK_BELOW_WIDTH };
5414
+ }
5415
+ function effectiveLayout(def, containerWidth, options = {}) {
5416
+ const shape = layoutShape(def.canvas, containerWidth, options.scale ?? 1);
5417
+ const items = [];
5418
+ for (const w of def.widgets) {
5419
+ const p = resolvePlacement(w.layout, shape.breakpoint);
5420
+ if (p.mode === "grid") items.push({ id: w.id, placement: p, limits: options.limitsOf?.(w) });
5421
+ }
5422
+ const grid = shape.reflowed ? reflowGrid(items, shape.columns, { stack: shape.stacked }) : new Map(items.map((i) => [i.id, i.placement]));
5423
+ return { ...shape, grid };
5424
+ }
3438
5425
  export {
5426
+ ANCHOR_PAGE_SIZE,
3439
5427
  BASE_BREAKPOINT,
5428
+ CANVAS_DENSITIES,
3440
5429
  COMMAND_STATUSES,
5430
+ CURRENT_SCHEMA_VERSION,
5431
+ ConnectionTracker,
5432
+ DEFAULT_CANVAS_DENSITY,
5433
+ DEFAULT_GRID,
5434
+ DEFAULT_HISTORY_LIMIT,
5435
+ DEFAULT_TIME_RANGE_SECONDS,
5436
+ DENSITY_SCALE,
3441
5437
  DashboardDefinitionError,
3442
5438
  DashboardHub,
5439
+ FALLBACK_DEFAULT_SPAN,
5440
+ FRAME_BORDER_WIDTHS,
5441
+ FRAME_HEADERS,
5442
+ FRAME_RADII,
5443
+ FRAME_VARIANTS,
3443
5444
  HOST_OPTION_CONTROLS,
5445
+ LEGACY_REFERENCE_WIDTH,
5446
+ MAX_SEED_BUCKETS,
5447
+ MAX_TIME_RANGE_SECONDS,
5448
+ MIN_TIME_RANGE_SECONDS,
3444
5449
  NotInRecordingError,
5450
+ OFFLINE_AFTER_MS,
5451
+ OFFSET_STEP,
5452
+ OFFSET_STEP_FINE,
5453
+ PHONE_MIN_COLUMNS,
5454
+ PLACEMENT_ANCHORS,
5455
+ PLACEMENT_MODES,
3445
5456
  PLAYBACK_RATES,
5457
+ PUBLISH_BLOCKING_LAYOUT_CODES,
3446
5458
  RECORDED_HISTORY_WINDOW_MS,
3447
5459
  RECORDING_FORMAT_VERSION,
3448
5460
  RECORDING_KIND,
5461
+ RESERVED_PLACEMENT_MODES,
3449
5462
  RecordedClock,
3450
5463
  RecordedDataSource,
3451
5464
  RecordingFormatError,
3452
5465
  SDK_API_VERSION,
5466
+ SPACE_TOKEN_PX,
5467
+ STACK_BELOW_WIDTH,
3453
5468
  SYNTHETIC_GENERATORS,
3454
5469
  SyntheticDataSource,
5470
+ THEME_COLOR_TOKENS,
5471
+ TIME_RANGE_PRESETS,
3455
5472
  UNKNOWN_WIDGET_TYPE,
5473
+ WIDGET_CLIPBOARD_FORMAT,
5474
+ WIDGET_CLIPBOARD_VERSION,
3456
5475
  WIDGET_TYPES,
3457
5476
  WidgetCatalogError,
3458
5477
  activeBreakpoint,
3459
5478
  addWidget,
5479
+ anchorFraction,
3460
5480
  anchorSlotNames,
3461
5481
  applyOptionPatch,
3462
5482
  applySelection,
3463
- baseBox,
5483
+ basePlacement,
3464
5484
  bindWidgetSlot,
3465
5485
  bindingsWithoutScopedSlots,
3466
- bringToFront,
5486
+ blockingLayoutFindings,
5487
+ breakpointColumns,
5488
+ breakpointMinWidth,
5489
+ bucketSecondsFor,
5490
+ canHostRelative,
5491
+ canRedo,
3467
5492
  canScopeSlot,
5493
+ canUndo,
5494
+ cellSizeAt,
3468
5495
  checkWidgetCompatibility,
3469
5496
  clampNumberOption,
5497
+ clampSpan,
3470
5498
  clearWidgetDatasource,
3471
5499
  collectCatalogIssues,
5500
+ commitEdit,
5501
+ containerWidthFor,
5502
+ copyWidget,
3472
5503
  createDeviceResolver,
5504
+ createEditHistory,
3473
5505
  createEntityLister,
3474
5506
  createRecordedClock,
3475
5507
  createRecordedLister,
@@ -3479,51 +5511,108 @@ export {
3479
5511
  defineWidgetDescriptor,
3480
5512
  deleteWidget,
3481
5513
  describeIssues,
5514
+ duplicateWidget,
3482
5515
  effectiveBindings,
5516
+ effectiveLayout,
3483
5517
  enumValues,
3484
5518
  fetchWidgetHistory,
5519
+ firstFreeSlot,
5520
+ fitToLimits,
5521
+ formatSpan,
3485
5522
  generateWidgetId,
3486
- gridBoxToPx,
5523
+ gridFootprintsOverlap,
5524
+ gridLengthPx,
5525
+ gridPlacementToPx,
5526
+ gridRect,
5527
+ gutterPx,
3487
5528
  hasScopedSlots,
5529
+ historyWindowFor,
5530
+ initialTimeRange,
3488
5531
  isBuiltinWidget,
3489
5532
  isBuiltinWidgetId,
3490
5533
  isCancellableCommandStatus,
5534
+ isCanvasDensity,
3491
5535
  isDirty,
3492
5536
  isNamespacedWidgetId,
3493
5537
  isResolvedWidget,
5538
+ isSpaceToken,
3494
5539
  isTerminalCommandStatus,
5540
+ isValidSpanSeconds,
5541
+ layoutBreakpoints,
5542
+ layoutRects,
5543
+ layoutShape,
5544
+ lintLayout,
5545
+ liveColumns,
3495
5546
  migrateToSlots,
5547
+ moveWidget,
3496
5548
  numberSpec,
5549
+ paddingPx,
3497
5550
  parseBindingManifest,
3498
5551
  parseBoardRecording,
3499
5552
  parseDashboardDefinition,
5553
+ parsePausedParam,
5554
+ parseRangeParam,
3500
5555
  parseSlotBinding,
5556
+ parseWidgetClipboard,
3501
5557
  parseWidgetRef,
5558
+ pasteWidgets,
5559
+ pauseRange,
5560
+ pinnedAxes,
5561
+ placeOnGrid,
5562
+ placeRelative,
5563
+ presetIdOf,
3502
5564
  pruneSlots,
3503
- pxToGridBox,
5565
+ pushAside,
5566
+ pushDown,
5567
+ pxToGridPlacement,
5568
+ rangeEnd,
5569
+ rangeWindow,
5570
+ rebaseHistory,
3504
5571
  recordedWallTimeMs,
3505
5572
  recordingBounds,
5573
+ redoEdit,
5574
+ reflowGrid,
5575
+ relativeRect,
5576
+ resizeWidget,
3506
5577
  resolveConcrete,
3507
5578
  resolveContextBindings,
5579
+ resolveFrame,
5580
+ resolvePlacement,
3508
5581
  resolveSlotCandidates,
3509
- resolveWidgetBox,
3510
5582
  resolveWidgetDatasource,
5583
+ resumeRange,
3511
5584
  sameBinding,
5585
+ sameLabel,
5586
+ sealHistory,
5587
+ seedBucketCount,
3512
5588
  serializeDefinition,
5589
+ serializeWidgetClipboard,
5590
+ setBoardTimeRange,
5591
+ setCanvasDensity,
3513
5592
  setCanvasGrid,
3514
5593
  setCanvasSizing,
5594
+ setRelativeAnchor,
5595
+ setRelativeOffset,
3515
5596
  setSlotScope,
3516
5597
  setTitle,
3517
- setWidgetBox,
5598
+ setWidgetPlacement,
3518
5599
  simulationScaleAt,
5600
+ snapOffset,
5601
+ spanLabel,
5602
+ spanLimitsOfLayout,
5603
+ spanPx,
3519
5604
  storedDefinition,
3520
5605
  stripDefaultBindings,
5606
+ timeRangeFromSearch,
5607
+ undoEdit,
3521
5608
  unresolvedWidgets,
3522
5609
  updateWidget,
3523
5610
  validateOptions,
3524
5611
  validateWidgetDescriptor,
3525
5612
  widgetBinding,
3526
5613
  widgetSlotName,
3527
- widgetVersionNotice
5614
+ widgetVersionNotice,
5615
+ withSpan,
5616
+ withoutConversionMark
3528
5617
  };
3529
5618
  //# sourceMappingURL=index.js.map