tickslate 0.0.0-stage → 0.1.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.
@@ -0,0 +1,4781 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ import { isSize, snapSize, toTime, toNumber, emptyLayout, WIDGETS, readLayout, DEFAULT_PROFILE, withProfile, addItem, nextProfileChange, activeProfile, columnsFor, resizeItem, typeForTag, place, footprint, dropIndex, moveItem, sizeForExtent, stepSize, removeItem, SIZE_NAMES, SIZES, currentConfig, createFormatter, equitySeries, equityStats, toDay, localDate, version } from "./index.js";
5
+ import { version as version2 } from "./index.js";
6
+ function metrics(width, columns, rowHeight, gap) {
7
+ const colWidth = Math.max(0, (width - gap * (columns - 1)) / columns);
8
+ return { columns, colWidth, rowHeight, gap, pitchX: colWidth + gap, pitchY: rowHeight + gap };
9
+ }
10
+ function boxOf(p, m) {
11
+ return {
12
+ x: p.col * m.pitchX,
13
+ y: p.row * m.pitchY,
14
+ w: p.cols * m.colWidth + (p.cols - 1) * m.gap,
15
+ h: p.rows * m.rowHeight + (p.rows - 1) * m.gap
16
+ };
17
+ }
18
+ function boardHeight(rows, m) {
19
+ return rows > 0 ? rows * m.rowHeight + (rows - 1) * m.gap : 0;
20
+ }
21
+ function cellAt(x, y, cols, m) {
22
+ const maxCol = Math.max(0, m.columns - cols);
23
+ return {
24
+ col: Math.min(maxCol, Math.max(0, x / m.pitchX)),
25
+ row: Math.max(0, y / m.pitchY)
26
+ };
27
+ }
28
+ function extentOf(w, h, m) {
29
+ return {
30
+ cols: Math.min(m.columns, Math.max(1, (w + m.gap) / m.pitchX)),
31
+ rows: Math.max(1, (h + m.gap) / m.pitchY)
32
+ };
33
+ }
34
+ function mergeOrder(full, visibleOrder) {
35
+ const visible = new Set(visibleOrder.map((item) => item.id));
36
+ let next = 0;
37
+ return full.map((item) => visible.has(item.id) ? visibleOrder[next++] : item);
38
+ }
39
+ function px(value, fallback) {
40
+ const n = parseFloat(value);
41
+ return Number.isFinite(n) && /px\s*$/.test(String(value).trim()) ? n : fallback;
42
+ }
43
+ const hasOwn$1 = (o, k2) => Object.prototype.hasOwnProperty.call(o, k2);
44
+ function itemsFromChildren(descriptors, resolve, reserved) {
45
+ const claimed = /* @__PURE__ */ new Set();
46
+ const ids = descriptors.map((d) => {
47
+ if (!d.item || claimed.has(d.item)) return null;
48
+ claimed.add(d.item);
49
+ return d.item;
50
+ });
51
+ const taken = /* @__PURE__ */ new Set([...claimed, ...reserved || []]);
52
+ return descriptors.map((d, i) => {
53
+ const { type, spec: catalogSpec } = resolve(d.tag);
54
+ const spec = d.spec || catalogSpec || null;
55
+ let id = ids[i];
56
+ if (id === null) {
57
+ id = type;
58
+ for (let n = 2; taken.has(id); n++) id = `${type}-${n}`;
59
+ taken.add(id);
60
+ }
61
+ const sizes = spec && Array.isArray(spec.sizes) && spec.sizes.length ? spec.sizes : null;
62
+ let size;
63
+ if (isSize(d.declaredSize)) size = sizes ? snapSize(d.declaredSize, sizes) : d.declaredSize;
64
+ else size = sizes ? sizes[0] : "S";
65
+ return id === type ? { id, size } : { id, type, size };
66
+ });
67
+ }
68
+ function reconcile(prevItems, current, removedIds) {
69
+ const removed = removedIds || /* @__PURE__ */ new Set();
70
+ const live = new Set(current.map((item) => item.id));
71
+ const kept = prevItems.filter((item) => live.has(item.id) && !removed.has(item.id));
72
+ const have = new Set(kept.map((item) => item.id));
73
+ return kept.concat(current.filter((item) => !have.has(item.id) && !removed.has(item.id)));
74
+ }
75
+ function specResolver(elementSpecs, hostCatalog, base) {
76
+ return (item) => elementSpecs.get(item.id) || lookup$1(item, hostCatalog) || lookup$1(item, base) || null;
77
+ }
78
+ function lookup$1(item, catalog) {
79
+ if (typeof catalog === "function") return catalog(item) || null;
80
+ if (!catalog) return null;
81
+ const type = item.type === void 0 ? item.id : item.type;
82
+ return typeof type === "string" && hasOwn$1(catalog, type) ? catalog[type] : null;
83
+ }
84
+ function hostResizes(entries) {
85
+ const out = /* @__PURE__ */ new Map();
86
+ for (const { id, size, written } of entries) {
87
+ if (written !== void 0 && size !== written && isSize(size)) out.set(id, size);
88
+ }
89
+ return out;
90
+ }
91
+ function elementSpec(el) {
92
+ if (!el) return null;
93
+ const own = el.spec;
94
+ if (own !== void 0) {
95
+ return own && typeof own === "object" && Array.isArray(own.sizes) && own.sizes.length ? own : null;
96
+ }
97
+ const attr = el.getAttribute("sizes");
98
+ if (!attr) return null;
99
+ const sizes = attr.split(/[\s,]+/).filter(isSize);
100
+ return sizes.length ? { label: el.getAttribute("label") || "", sizes } : null;
101
+ }
102
+ const DEV = true;
103
+ const warned = /* @__PURE__ */ new Set();
104
+ function warnOnce(key, message) {
105
+ if (warned.has(key)) return;
106
+ warned.add(key);
107
+ console.warn("tickslate: " + message);
108
+ }
109
+ const KINDS = ["number", "numeric", "time", "string", "boolean", "array", "object"];
110
+ const FIELD_KEYS = ["kind", "optional", "of"];
111
+ const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
112
+ const isField = (spec) => isObject(spec) && typeof spec.kind === "string" && KINDS.includes(spec.kind) && Object.keys(spec).every((k2) => FIELD_KEYS.includes(k2));
113
+ function received(v) {
114
+ if (v === void 0) return "missing";
115
+ if (v === null) return "null";
116
+ if (Array.isArray(v)) return "array";
117
+ if (typeof v === "string") return `string "${v.length > 24 ? v.slice(0, 24) + "…" : v}"`;
118
+ if (typeof v === "number" || typeof v === "boolean") return `${typeof v} ${v}`;
119
+ return typeof v;
120
+ }
121
+ function passes(kind, v) {
122
+ switch (kind) {
123
+ case "number":
124
+ return typeof v === "number" && Number.isFinite(v);
125
+ case "numeric":
126
+ return Number.isFinite(toNumber(v));
127
+ case "time":
128
+ return Number.isFinite(toTime(v));
129
+ case "string":
130
+ return typeof v === "string";
131
+ case "boolean":
132
+ return typeof v === "boolean";
133
+ case "array":
134
+ return Array.isArray(v);
135
+ default:
136
+ return isObject(v);
137
+ }
138
+ }
139
+ function checkValue(value, spec, path, parent) {
140
+ if (typeof spec === "string") spec = { kind: spec };
141
+ else if (!isField(spec)) spec = { kind: "object", of: spec };
142
+ if (value === void 0 || value === null) {
143
+ if (spec.optional) return null;
144
+ const problem = { path, expected: spec.kind, received: received(value) };
145
+ if (parent) problem.keys = Object.keys(parent);
146
+ return problem;
147
+ }
148
+ if (!passes(spec.kind, value)) return { path, expected: spec.kind, received: received(value) };
149
+ if (spec.of === void 0) return null;
150
+ if (spec.kind === "array") {
151
+ for (let i = 0; i < value.length; i++) {
152
+ const problem = checkValue(value[i], spec.of, `${path}[${i}]`, isObject(value[i]) ? value[i] : null);
153
+ if (problem) return problem;
154
+ }
155
+ } else if (spec.kind === "object" && isObject(spec.of) && !isField(spec.of)) {
156
+ return checkShape(value, spec.of, path);
157
+ }
158
+ return null;
159
+ }
160
+ function checkShape(data, shape, path = "data") {
161
+ if (!isObject(data)) return { path, expected: "object", received: received(data) };
162
+ for (const key of Object.keys(shape)) {
163
+ const problem = checkValue(data[key], shape[key], `${path}.${key}`, data);
164
+ if (problem) return problem;
165
+ }
166
+ return null;
167
+ }
168
+ function shapeMessage(problem) {
169
+ if (problem.received === "missing") {
170
+ return `${problem.path} is missing (got keys: ${problem.keys && problem.keys.length ? problem.keys.join(", ") : "none"})`;
171
+ }
172
+ return `${problem.path} should be ${problem.expected} (got ${problem.received})`;
173
+ }
174
+ function parseErrorParts(err) {
175
+ const raw = String(err && err.message || err);
176
+ const at = /position (\d+)/.exec(raw);
177
+ const message = raw.replace(/\s*(in JSON )?at position \d+(\s*\(line \d+ column \d+\))?/, "");
178
+ return { message, position: at ? Number(at[1]) : null };
179
+ }
180
+ function createHistory(cap = 20) {
181
+ const limit = Math.max(1, Math.floor(cap) || 20);
182
+ let stacks = /* @__PURE__ */ Object.create(null);
183
+ return {
184
+ /** Remember `layout` as the state to return to for `profile`. The oldest entry goes past the cap. */
185
+ push(profile, layout) {
186
+ const stack = stacks[profile] || (stacks[profile] = []);
187
+ stack.push(layout);
188
+ if (stack.length > limit) stack.shift();
189
+ },
190
+ /** The most recent layout for `profile`, removed from the history, or null when there is none. */
191
+ undo(profile) {
192
+ const stack = stacks[profile];
193
+ return stack && stack.length ? stack.pop() : null;
194
+ },
195
+ /** Forget everything, as when the host sets a layout of its own. */
196
+ clear() {
197
+ stacks = /* @__PURE__ */ Object.create(null);
198
+ }
199
+ };
200
+ }
201
+ function isOutside(path, grid, extras = []) {
202
+ if (!path) return true;
203
+ for (const node of path) {
204
+ if (node === grid || extras.includes(node)) return false;
205
+ }
206
+ return true;
207
+ }
208
+ function pickFocusTarget(candidates) {
209
+ for (const c of candidates) {
210
+ if (c && c.connected && !c.hidden && !c.disabled) return c.target;
211
+ }
212
+ return null;
213
+ }
214
+ const ARROWS = {
215
+ ArrowLeft: [-1, 0],
216
+ ArrowRight: [1, 0],
217
+ ArrowUp: [0, -1],
218
+ ArrowDown: [0, 1]
219
+ };
220
+ const isActivate = (key) => key === " " || key === "Enter" || key === "Spacebar";
221
+ function keyAction(key, focusedId, state) {
222
+ const held = state && state.held ? state.held : null;
223
+ if (held) {
224
+ if (isActivate(key)) return { type: "drop", id: held };
225
+ if (key === "Escape") return { type: "cancel", id: held };
226
+ const arrow = ARROWS[key];
227
+ if (arrow) return { type: "moveBy", id: held, dCol: arrow[0], dRow: arrow[1] };
228
+ return null;
229
+ }
230
+ if (!focusedId) return null;
231
+ if (isActivate(key)) return { type: "pickUp", id: focusedId };
232
+ if (key === "+" || key === "=") return { type: "resizeStep", id: focusedId, dir: 1 };
233
+ if (key === "-" || key === "_") return { type: "resizeStep", id: focusedId, dir: -1 };
234
+ if (key === "Delete" || key === "Backspace") return { type: "remove", id: focusedId };
235
+ return null;
236
+ }
237
+ function tapAction(tappedId, state) {
238
+ const held = state && state.held ? state.held : null;
239
+ if (held) {
240
+ if (!tappedId) return { type: "cancel", id: held };
241
+ if (tappedId === held) return { type: "drop", id: held };
242
+ return { type: "placeAt", id: held, targetId: tappedId };
243
+ }
244
+ return tappedId ? { type: "pickUp", id: tappedId } : null;
245
+ }
246
+ const LONG_PRESS_MS = 500;
247
+ const PRESS_SLOP = 8;
248
+ const MOUSE_DRAG_SLOP = 4;
249
+ const TOUCH_HOLD_MS = 200;
250
+ const SETTLE_MS = 120;
251
+ const EDGE = 64;
252
+ const MAX_SCROLL = 900;
253
+ const LIFT_SCALE = 1.04;
254
+ const SLIDE = "var(--ts-motion, 220ms) var(--ts-ease, cubic-bezier(.2, .8, .2, 1))";
255
+ const MAX_WAIT = 6e4;
256
+ const UNDEFINED_TAG_MS = 2e3;
257
+ const NOT_TILES = /* @__PURE__ */ new Set(["script", "style", "template", "link", "meta", "noscript"]);
258
+ const NO_SPECS = /* @__PURE__ */ new Map();
259
+ const REMOVE_GUARD_MS = 400;
260
+ const TOAST_MS = 6e3;
261
+ const TEXT = {
262
+ removed: (label) => `${label} removed`,
263
+ undo: "Undo",
264
+ editLayout: "Edit layout",
265
+ done: "Done",
266
+ addBack: (n) => `Add back (${n})`,
267
+ addBackItem: (label) => `Add back ${label}`,
268
+ addedBack: (label) => `${label} added back.`,
269
+ hiddenHeading: "Hidden from this layout",
270
+ editingGroup: "Editing layout",
271
+ emptyBoard: "No widgets on this layout yet",
272
+ dashboard: "Dashboard",
273
+ editStart: (n) => `Editing layout, ${n} ${n === 1 ? "tile" : "tiles"}. Tab to a tile, Space to pick it up.`,
274
+ editDone: "Done editing layout.",
275
+ handle: (label, size, n, total) => `${label}, ${size}, position ${n} of ${total}`,
276
+ instructions: "Space or Enter picks the tile up, then the arrow keys move it and Space drops it. Plus and minus resize it. Delete removes it. You can also tap the tile, then tap where it should go.",
277
+ removeLabel: (label) => `Remove ${label}`,
278
+ resizeLabel: (label, size) => `Resize ${label}, now ${size}`,
279
+ pickedUp: (label) => `${label}, picked up. Use the arrow keys to move it, Space to drop, Escape to cancel.`,
280
+ moved: (label, n, total) => `${label}, moved to position ${n} of ${total}.`,
281
+ dropped: (label) => `${label}, dropped.`,
282
+ droppedAt: (label, n, total) => `${label}, dropped at position ${n} of ${total}.`,
283
+ cancelled: (label) => `${label}, move cancelled.`,
284
+ blocked: "Can't move further.",
285
+ resized: (label, size) => `${label}, size ${size}.`,
286
+ profile: (name) => `Showing the ${name} layout.`,
287
+ undone: "Undone.",
288
+ lockedWarning: "<ts-grid> is locked, so editing = true was ignored. Remove the locked attribute to allow editing."
289
+ };
290
+ const hasOwn = (o, k2) => Object.prototype.hasOwnProperty.call(o, k2);
291
+ let template = null;
292
+ const TEMPLATE$1 = `
293
+ <style>
294
+ :host { display: block; position: relative; -webkit-tap-highlight-color: transparent; }
295
+ :host([hidden]) { display: none; }
296
+ :host([dragging]) { cursor: grabbing; }
297
+ .board { position: relative; transition: height ${SLIDE}; }
298
+ /* A long press must never start a text selection on a touch screen, but a
299
+ mouse user may copy a figure out of a tile until they start editing. */
300
+ @media (pointer: coarse) {
301
+ :host(:not([locked])) ::slotted(*) { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
302
+ }
303
+ :host([editing]) ::slotted(*), :host([dragging]) ::slotted(*) { -webkit-user-select: none; user-select: none; }
304
+ .placeholder {
305
+ position: absolute; top: 0; left: 0; box-sizing: border-box; pointer-events: none;
306
+ border-radius: var(--ts-radius, 10px);
307
+ background: color-mix(in srgb, var(--ts-ink, #1C1A16) 4%, transparent);
308
+ border: 1.5px dashed color-mix(in srgb, var(--ts-ink, #1C1A16) 22%, transparent);
309
+ opacity: 0; transition: opacity 120ms ease;
310
+ }
311
+ .placeholder.on { opacity: 1; }
312
+ .chromes { position: absolute; inset: 0; pointer-events: none; }
313
+ /* The container never takes a pointer; its three buttons do, while editing. */
314
+ .chrome {
315
+ position: absolute; top: 0; left: 0; box-sizing: border-box; pointer-events: none;
316
+ border-radius: var(--ts-radius, 10px); z-index: 2;
317
+ }
318
+ .chrome button { font: inherit; color: inherit; }
319
+ .handle {
320
+ position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; border-radius: inherit;
321
+ background: none; pointer-events: none; cursor: grab; -webkit-tap-highlight-color: transparent;
322
+ }
323
+ :host([editing]) .handle, :host([editing]) .remove, :host([editing]) .resize { pointer-events: auto; }
324
+ :host([locked]) .chrome button { pointer-events: none; }
325
+ .chrome.removing .handle, .chrome.removing .remove, .chrome.removing .resize { pointer-events: none; }
326
+ .chrome.removing .remove { display: none; }
327
+ :host([dragging]) .handle { cursor: grabbing; }
328
+ /* An outline, not a shadow, so the ring survives forced colours. */
329
+ .handle:focus-visible, .resize:focus-visible { outline: 2px solid var(--ts-focus); outline-offset: 2px; }
330
+ .chrome.held .handle { outline: 2px solid var(--ts-focus); outline-offset: 2px; }
331
+ .remove, .resize { display: none; }
332
+ :host([editing]) .remove, :host([editing]) .resize { display: flex; }
333
+ .chrome.lifted .remove, .chrome.lifted .resize, .chrome.fixed .resize { display: none; }
334
+ .remove {
335
+ position: absolute; top: -16px; left: -16px; width: 44px; height: 44px; padding: 0; margin: 0;
336
+ border: 0; background: none; align-items: center; justify-content: center; cursor: pointer;
337
+ animation: pop 160ms var(--ts-ease, ease-out);
338
+ }
339
+ .remove span {
340
+ width: 22px; height: 22px; border-radius: 50%; box-sizing: border-box;
341
+ display: flex; align-items: center; justify-content: center;
342
+ background: var(--ts-surface-2, #F1EBDE); color: var(--ts-ink, #1C1A16);
343
+ border: 1px solid var(--ts-line, #E2DACB); box-shadow: var(--ts-shadow, none);
344
+ font: 600 16px/1 var(--ts-font-ui, system-ui, sans-serif);
345
+ }
346
+ .remove:focus-visible span { outline: 2px solid var(--ts-focus); outline-offset: 2px; }
347
+ .resize {
348
+ position: absolute; right: -6px; bottom: -6px; width: 44px; height: 44px; margin: 0; padding: 0; border: 0;
349
+ background: none; cursor: nwse-resize; touch-action: none; animation: pop 160ms var(--ts-ease, ease-out);
350
+ }
351
+ .resize::after {
352
+ content: ''; position: absolute; right: 12px; bottom: 12px; width: 12px; height: 12px;
353
+ border-right: 3px solid var(--ts-muted, #6A655B); border-bottom: 3px solid var(--ts-muted, #6A655B);
354
+ border-bottom-right-radius: 6px;
355
+ }
356
+ @keyframes pop { from { opacity: 0; scale: .6; } to { opacity: 1; scale: 1; } }
357
+ @media (prefers-reduced-motion: reduce) {
358
+ /* The wiggle is gone, so this outline alone says "editing". The RFC's 45% mix is
359
+ 2.8:1 against Paper's --ts-bg, under the 3:1 it asks for; 50% clears it in both themes. */
360
+ :host([editing]) .chrome { outline: 2px dashed color-mix(in srgb, var(--ts-ink) 50%, transparent); outline-offset: -1px; }
361
+ .remove, .resize { animation: none; }
362
+ }
363
+ [hidden] { display: none !important; }
364
+ /* The dock holds the pill and, above it, the undo toast, so they never overlap.
365
+ It has no height of its own until the pill shows, then it reserves the pill's
366
+ room: below the grid when it all fits, floating over the tiles when it doesn't. */
367
+ .dock { position: sticky; bottom: 22px; height: 0; z-index: 7; pointer-events: none; }
368
+ .dock.has-pill { height: 56px; margin-top: 22px; }
369
+ .stack {
370
+ position: absolute; inset-inline: 0; bottom: 0; display: flex; flex-direction: column; align-items: center;
371
+ gap: 10px; pointer-events: none;
372
+ }
373
+ .toast {
374
+ position: relative; display: inline-flex; align-items: center; gap: 0; padding: 4px 4px 4px 20px;
375
+ border-radius: 999px; box-sizing: border-box; max-width: calc(100% - 32px); white-space: nowrap;
376
+ background: var(--ts-ink); color: var(--ts-surface); box-shadow: var(--ts-shadow-lift);
377
+ font: 500 13px var(--ts-font-ui); pointer-events: auto;
378
+ }
379
+ /* Kept in the tree while off, so the status region announces the next text put in it. */
380
+ .toast:not(.on) { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; opacity: 0; pointer-events: none; }
381
+ .toast .msg { overflow: hidden; text-overflow: ellipsis; }
382
+ .toast .sep { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
383
+ .toast .undo {
384
+ min-width: 44px; min-height: 44px; margin: 0 0 0 16px; padding: 0 18px; border: 0; border-radius: 999px; cursor: pointer;
385
+ background: color-mix(in srgb, var(--ts-surface) 14%, transparent); color: inherit; font: inherit; font-weight: 600;
386
+ }
387
+ .toast .undo:focus-visible { outline: 2px solid var(--ts-surface); outline-offset: 2px; }
388
+ .pill {
389
+ position: relative; display: inline-flex; align-items: center; gap: 4px; padding: 5px; border-radius: 999px;
390
+ box-sizing: border-box; max-width: calc(100% - 32px); pointer-events: auto;
391
+ background: var(--ts-surface); color: var(--ts-ink); border: 1px solid var(--ts-line); box-shadow: var(--ts-shadow-lift);
392
+ }
393
+ .pb {
394
+ min-width: 44px; min-height: 44px; margin: 0; padding: 0 20px; border: 0; border-radius: 999px; cursor: pointer;
395
+ display: inline-flex; align-items: center; justify-content: center; gap: 8px; white-space: nowrap;
396
+ background: none; color: var(--ts-ink); font: 600 13px var(--ts-font-ui);
397
+ }
398
+ .pb:focus-visible, .mi:focus-visible, .edit-btn:focus-visible { outline: 2px solid var(--ts-focus); outline-offset: 2px; }
399
+ .pb[aria-expanded="true"] { background: var(--ts-surface-2); }
400
+ .pb.done { padding-inline: 24px; background: var(--ts-accent); color: var(--ts-surface); }
401
+ .pb.ghost { border: 1px solid var(--ts-line); background: var(--ts-surface); }
402
+ .pb .plus { color: var(--ts-accent); font-size: 16px; line-height: 1; }
403
+ .menu {
404
+ position: absolute; bottom: calc(100% + 8px); inset-inline-start: 0; z-index: 3; box-sizing: border-box;
405
+ width: 250px; max-width: calc(100vw - 32px); max-height: 50vh; overflow-y: auto; padding: 6px;
406
+ display: flex; flex-direction: column; text-align: start;
407
+ background: var(--ts-surface); color: var(--ts-ink); border: 1px solid var(--ts-line);
408
+ border-radius: var(--ts-radius); box-shadow: var(--ts-shadow-lift); pointer-events: auto;
409
+ }
410
+ .menu-h { padding: 8px 10px 6px; font: 600 10px var(--ts-font-ui); letter-spacing: .12em; text-transform: uppercase; color: var(--ts-muted); }
411
+ .mi {
412
+ display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; min-height: 44px;
413
+ margin: 0; padding: 0 10px; border: 0; border-radius: var(--ts-radius-sm); cursor: pointer; text-align: start;
414
+ background: none; color: inherit; font: 13px var(--ts-font-ui);
415
+ }
416
+ .mi:hover { background: var(--ts-surface-2); }
417
+ .mi .plus { color: var(--ts-accent); font-size: 16px; }
418
+ /* Visually hidden until a keyboard user reaches it, so it is the first Tab stop without crowding the dashboard. */
419
+ .edit-btn {
420
+ position: absolute; top: 12px; left: 12px; z-index: 8; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
421
+ overflow: hidden; clip-path: inset(50%); white-space: nowrap;
422
+ }
423
+ .edit-btn:focus-visible {
424
+ width: auto; height: auto; min-height: 44px; margin: 0; padding: 0 18px; overflow: visible; clip-path: none;
425
+ border: 1px solid var(--ts-line); border-radius: 999px; cursor: pointer;
426
+ background: var(--ts-surface); color: var(--ts-ink); font: 600 13px var(--ts-font-ui); box-shadow: var(--ts-shadow-lift);
427
+ }
428
+ .empty {
429
+ position: absolute; inset: 0; box-sizing: border-box; display: flex; flex-direction: column; align-items: center;
430
+ justify-content: center; gap: 12px; padding: 16px; text-align: center; color: var(--ts-ink); font: 500 14px var(--ts-font-ui);
431
+ border: 1.5px dashed color-mix(in srgb, var(--ts-ink) 22%, transparent); border-radius: var(--ts-radius);
432
+ background: color-mix(in srgb, var(--ts-ink) 4%, transparent);
433
+ }
434
+ .empty .note { margin: 0; }
435
+ .empty .anchor { position: relative; }
436
+ .empty .menu { bottom: auto; top: calc(100% + 8px); inset-inline-start: 50%; transform: translateX(-50%); }
437
+ .hint { display: none; }
438
+ .sr { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
439
+ </style>
440
+ <button class="edit-btn" type="button" hidden></button>
441
+ <div class="board" part="board">
442
+ <div class="placeholder" part="placeholder"></div>
443
+ <slot></slot>
444
+ <div class="chromes"></div>
445
+ <div class="empty" part="empty" hidden>
446
+ <slot name="empty"><p class="note"></p></slot>
447
+ <div class="anchor"><button class="pb ghost add" type="button" aria-expanded="false" hidden><span class="plus" aria-hidden="true">+</span><span class="t"></span></button><div class="menu" role="group" hidden></div></div>
448
+ </div>
449
+ </div>
450
+ <div class="dock"><div class="stack">
451
+ <div class="toast" part="toast" role="status"><span class="msg"></span><span class="sep"> · </span><button class="undo" type="button" tabindex="-1"></button></div>
452
+ <div class="pill" part="edit-pill" role="group" hidden>
453
+ <button class="pb add" type="button" aria-expanded="false" hidden><span class="plus" aria-hidden="true">+</span><span class="t"></span></button>
454
+ <button class="pb done" type="button"></button>
455
+ <div class="menu" role="group" hidden></div>
456
+ </div>
457
+ </div></div>
458
+ <div class="sr" role="status" aria-live="polite"></div>
459
+ <p class="hint" id="hint"></p>
460
+ `;
461
+ const Base$1 = typeof HTMLElement === "undefined" ? class {
462
+ } : HTMLElement;
463
+ class TsGrid extends Base$1 {
464
+ static get observedAttributes() {
465
+ return ["editing", "profile", "locked", "edit-controls"];
466
+ }
467
+ constructor() {
468
+ super();
469
+ if (!template) {
470
+ template = document.createElement("template");
471
+ template.innerHTML = TEMPLATE$1;
472
+ }
473
+ const root = this.attachShadow({ mode: "open" });
474
+ root.appendChild(template.content.cloneNode(true));
475
+ this._board = root.querySelector(".board");
476
+ this._placeholder = root.querySelector(".placeholder");
477
+ this._chromeLayer = root.querySelector(".chromes");
478
+ this._status = root.querySelector(".sr");
479
+ this._toast = root.querySelector(".toast");
480
+ this._toastText = root.querySelector(".toast .msg");
481
+ this._toastUndo = root.querySelector(".toast .undo");
482
+ this._toastUndo.textContent = TEXT.undo;
483
+ this._editBtn = root.querySelector(".edit-btn");
484
+ this._editBtn.textContent = TEXT.editLayout;
485
+ this._dock = root.querySelector(".dock");
486
+ this._pill = root.querySelector(".pill");
487
+ this._pill.setAttribute("aria-label", TEXT.editingGroup);
488
+ this._pill.querySelector(".done").textContent = TEXT.done;
489
+ this._emptyPanel = root.querySelector(".empty");
490
+ this._emptyPanel.querySelector(".note").textContent = TEXT.emptyBoard;
491
+ this._addBack = {
492
+ pill: { add: this._pill.querySelector(".add"), menu: this._pill.querySelector(".menu"), wrap: this._pill },
493
+ empty: { add: this._emptyPanel.querySelector(".add"), menu: this._emptyPanel.querySelector(".menu"), wrap: this._emptyPanel.querySelector(".anchor") }
494
+ };
495
+ this._menuOwner = null;
496
+ this._menuSig = "";
497
+ this._editOrigin = null;
498
+ this._toastState = null;
499
+ this._history = createHistory();
500
+ this._removing = /* @__PURE__ */ new Set();
501
+ this._removedAt = -Infinity;
502
+ root.querySelector(".hint").textContent = TEXT.instructions;
503
+ this._inertWas = /* @__PURE__ */ new Map();
504
+ this._deferred = false;
505
+ this._said = [];
506
+ this._layout = emptyLayout();
507
+ this._catalog = WIDGETS;
508
+ this._explicit = false;
509
+ this._removed = /* @__PURE__ */ new Map();
510
+ this._declared = /* @__PURE__ */ new WeakMap();
511
+ this._written = /* @__PURE__ */ new WeakMap();
512
+ this._itemOf = /* @__PURE__ */ new WeakMap();
513
+ this._columnsSeen = false;
514
+ this._watching = /* @__PURE__ */ new Set();
515
+ this._watchedTags = /* @__PURE__ */ new Set();
516
+ this._elementSpecs = { get: (id) => elementSpec(this._tiles.get(id)) };
517
+ this._setCatalog(WIDGETS);
518
+ this._columns = 0;
519
+ this._m = null;
520
+ this._order = [];
521
+ this._tiles = /* @__PURE__ */ new Map();
522
+ this._chromes = /* @__PURE__ */ new Map();
523
+ this._boxes = /* @__PURE__ */ new Map();
524
+ this._placements = /* @__PURE__ */ new Map();
525
+ this._wiggles = /* @__PURE__ */ new Map();
526
+ this._gesture = null;
527
+ this._rendered = false;
528
+ this._editing = false;
529
+ this._suppressClick = false;
530
+ this._schedule = null;
531
+ this._scheduled = null;
532
+ this._override = null;
533
+ this._profileTimer = 0;
534
+ this._clock = () => Date.now();
535
+ this._onVisibility = () => {
536
+ if (document.visibilityState === "visible") this._reschedule(true);
537
+ };
538
+ this._motionQuery = typeof matchMedia === "function" ? matchMedia("(prefers-reduced-motion: reduce)") : null;
539
+ this._onPointerDown = this._onPointerDown.bind(this);
540
+ this._onPointerMove = this._onPointerMove.bind(this);
541
+ this._onPointerUp = this._onPointerUp.bind(this);
542
+ this._onPointerCancel = this._onPointerCancel.bind(this);
543
+ this._onKeyDown = this._onKeyDown.bind(this);
544
+ this._onDocKeyDown = this._onDocKeyDown.bind(this);
545
+ this._onDocPointerDown = this._onDocPointerDown.bind(this);
546
+ this._onMenuPointerDown = this._onMenuPointerDown.bind(this);
547
+ this._frame = this._frame.bind(this);
548
+ this.addEventListener("pointerdown", this._onPointerDown);
549
+ this.addEventListener("keydown", this._onKeyDown);
550
+ this.addEventListener("keyup", (e) => {
551
+ if (this._editing && (e.key === " " || e.key === "Spacebar") && this._handleOf(e)) e.preventDefault();
552
+ });
553
+ this.addEventListener("touchmove", (e) => {
554
+ if (this._gesture && (this._gesture.kind === "drag" || this._gesture.kind === "resize")) e.preventDefault();
555
+ }, { passive: false });
556
+ this.addEventListener("contextmenu", (e) => {
557
+ if (this._gesture) e.preventDefault();
558
+ });
559
+ this.addEventListener("click", (e) => {
560
+ if (this._suppressClick) {
561
+ e.preventDefault();
562
+ e.stopPropagation();
563
+ }
564
+ }, true);
565
+ root.addEventListener("click", (e) => {
566
+ const target = e.target.closest ? e.target : null;
567
+ if (!target) return;
568
+ const button = target.closest(".remove");
569
+ const handle = target.closest(".handle");
570
+ const resize = target.closest(".resize");
571
+ if (button) this._remove(button.parentElement.dataset.id);
572
+ else if (handle) this._tap(handle.parentElement.dataset.id);
573
+ else if (resize) this._resizeClick(resize.parentElement.dataset.id);
574
+ else if (target === this._board) this._tap(null);
575
+ else if (target.closest(".undo")) this._undoFromToast();
576
+ else if (target.closest(".edit-btn")) {
577
+ if (this.locked) return;
578
+ this._editOrigin = this._editBtn;
579
+ this.editing = true;
580
+ } else if (target.closest(".done")) this.editing = false;
581
+ else if (target.closest(".add")) this._toggleMenu(target.closest(".pill") ? "pill" : "empty");
582
+ else if (target.closest(".mi")) this._restoreFromMenu(target.closest(".mi").dataset.id);
583
+ });
584
+ root.addEventListener("keydown", (e) => {
585
+ if (!this._menuOwner) return;
586
+ if (e.key === "Escape") {
587
+ e.preventDefault();
588
+ this._closeMenu(true);
589
+ } else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
590
+ const items = [...this._addBack[this._menuOwner].menu.querySelectorAll(".mi")];
591
+ const at = items.indexOf(root.activeElement);
592
+ if (at < 0 && root.activeElement !== this._addBack[this._menuOwner].add) return;
593
+ e.preventDefault();
594
+ const next = at < 0 ? e.key === "ArrowDown" ? 0 : items.length - 1 : (at + (e.key === "ArrowDown" ? 1 : -1) + items.length) % items.length;
595
+ if (items[next]) items[next].focus();
596
+ }
597
+ });
598
+ const toastHold = (key, on) => () => {
599
+ const t = this._toastState;
600
+ if (!t) return;
601
+ t[key] = on;
602
+ this._toastTimer();
603
+ };
604
+ this._toast.addEventListener("pointerenter", toastHold("hover", true));
605
+ this._toast.addEventListener("pointerleave", toastHold("hover", false));
606
+ this._toast.addEventListener("focusin", toastHold("focus", true));
607
+ this._toast.addEventListener("focusout", toastHold("focus", false));
608
+ }
609
+ // ── Public API ─────────────────────────────────────────────────────────
610
+ get layout() {
611
+ return this._layout;
612
+ }
613
+ /** A Layout or an item array. Setting it ends implicit mode for good. */
614
+ set layout(value) {
615
+ this._explicit = true;
616
+ this._history.clear();
617
+ this._hideToast(false);
618
+ this._apply(value);
619
+ }
620
+ get catalog() {
621
+ return this._catalog;
622
+ }
623
+ set catalog(value) {
624
+ this._setCatalog(value || WIDGETS);
625
+ this._apply(this._layout);
626
+ }
627
+ _setCatalog(catalog) {
628
+ this._catalog = catalog;
629
+ this._resolve = specResolver(this._elementSpecs, catalog, WIDGETS);
630
+ this._typeSpec = specResolver(NO_SPECS, catalog, WIDGETS);
631
+ }
632
+ /** Read a layout from the host and show it. Issues are reported only here, never for the grid's own edits. */
633
+ _apply(value) {
634
+ this._matchChildren(false);
635
+ const columns = this._columns;
636
+ const device = columns ? columns === 2 ? "phone" : "desktop" : void 0;
637
+ const { layout, issues } = readLayout(value, { catalog: this._resolve, unknown: "keep", device });
638
+ if (issues.length) this._emit("ts-layout-issues", { issues });
639
+ this._cancelGesture();
640
+ this._layout = layout;
641
+ this._sync(this._rendered);
642
+ }
643
+ /** The profile shown: the `profile` attribute, else an override, else the schedule, else 'market'. */
644
+ get profile() {
645
+ return this.getAttribute("profile") || this._override && this._override.profile || this._scheduled || DEFAULT_PROFILE;
646
+ }
647
+ set profile(value) {
648
+ if (value) this.setAttribute("profile", value);
649
+ else this.removeAttribute("profile");
650
+ }
651
+ get editing() {
652
+ return this.hasAttribute("editing");
653
+ }
654
+ set editing(value) {
655
+ this.toggleAttribute("editing", !!value);
656
+ }
657
+ /** Read-only dashboard: no long press, no edit mode, no chromes. */
658
+ get locked() {
659
+ return this.hasAttribute("locked");
660
+ }
661
+ set locked(value) {
662
+ this.toggleAttribute("locked", !!value);
663
+ }
664
+ get columns() {
665
+ return this._columns;
666
+ }
667
+ /** Children whose item id isn't in the active profile, which includes tiles the user removed. */
668
+ get hiddenItems() {
669
+ const shown = new Set(this._profileItems().map((item) => item.id));
670
+ const seen = /* @__PURE__ */ new Set();
671
+ const list = [];
672
+ for (const el of this.children) {
673
+ if (!isTile(el)) continue;
674
+ const id = el.getAttribute("item");
675
+ if (!id || shown.has(id) || seen.has(id)) continue;
676
+ seen.add(id);
677
+ const type = this._typeFor(el.localName).type;
678
+ list.push({ id, type, label: this._labelFor(el, this._resolve({ id, type })) });
679
+ }
680
+ return list;
681
+ }
682
+ /**
683
+ * Put the active profile back the way it was before the last edit. A host
684
+ * can call this even while locked: the lock is for the user's hands.
685
+ */
686
+ undo() {
687
+ const profile = this.profile;
688
+ const before = this._profileItems();
689
+ const snapshot = this._history.undo(profile);
690
+ if (!snapshot) return false;
691
+ const items = hasOwn(snapshot.profiles, profile) && Array.isArray(snapshot.profiles[profile]) ? snapshot.profiles[profile] : [];
692
+ this._cancelGesture();
693
+ this._hideToast(false);
694
+ const now = new Set(items.map((item) => item.id));
695
+ const removed = this._removedFor(profile);
696
+ for (const item of before) if (!now.has(item.id)) removed.add(item.id);
697
+ for (const id of now) removed.delete(id);
698
+ const previous = this._layout;
699
+ this._layout = withProfile(this._layout, profile, items);
700
+ this._sync(true);
701
+ this._emit("ts-layout-change", { layout: this._layout, items: this._profileItems(), previous, profile, reason: "undo", id: "" });
702
+ return true;
703
+ }
704
+ /** Add a hidden child's item back, at the end of the profile; false when no hidden child has that id. */
705
+ restore(id) {
706
+ const hidden = this.hiddenItems.find((h) => h.id === id);
707
+ if (!hidden) return false;
708
+ const el = [...this.children].find((c) => isTile(c) && c.getAttribute("item") === id);
709
+ const spec = this._resolve({ id, type: hidden.type });
710
+ const sizes = spec && Array.isArray(spec.sizes) && spec.sizes.length ? spec.sizes : null;
711
+ const declared = this._declared.get(el) || el.getAttribute("size");
712
+ const size = sizes ? sizes.includes(declared) ? declared : sizes[0] : declared || "S";
713
+ const item = id === hidden.type ? { id, size } : { id, type: hidden.type, size };
714
+ const profile = this.profile;
715
+ const previous = this._layout;
716
+ const list = this._profileItems();
717
+ this._history.push(profile, previous);
718
+ this._removedFor(profile).delete(id);
719
+ this._layout = withProfile(this._layout, profile, addItem(list, item, this._resolve));
720
+ this._hideToast(false);
721
+ this._sync(true);
722
+ const items = this._profileItems();
723
+ const index = items.findIndex((it) => it.id === id);
724
+ this._emit("ts-layout-change", { layout: this._layout, items, previous, profile, reason: "add", id, index, item: items[index] });
725
+ return true;
726
+ }
727
+ /** { calendar, market: { sessions, leadMinutes?, lagMinutes? } }; switches profiles on time. */
728
+ get schedule() {
729
+ return this._schedule;
730
+ }
731
+ set schedule(value) {
732
+ const before = this.profile;
733
+ this._schedule = value || null;
734
+ if (!value) this._override = null;
735
+ this._reschedule(false);
736
+ this._profileChanged(before, "schedule", true);
737
+ }
738
+ /** What the grid takes the time to be. Tests and demos can replace it. */
739
+ get clock() {
740
+ return this._clock;
741
+ }
742
+ set clock(fn) {
743
+ this._clock = typeof fn === "function" ? fn : () => Date.now();
744
+ this._reschedule(true, "schedule", true);
745
+ }
746
+ /**
747
+ * Show `profile` until the schedule next changes, then go back to it: a
748
+ * manual switch the user makes from the dashboard. With no schedule it lasts
749
+ * until cleared with override(null). The `profile` attribute, if set, still wins.
750
+ */
751
+ override(profile) {
752
+ if (!profile) {
753
+ const before2 = this.profile;
754
+ this._override = null;
755
+ this._reschedule(false);
756
+ this._profileChanged(before2, "override-ended");
757
+ return;
758
+ }
759
+ const now = this._clock();
760
+ const until = this._schedule ? nextProfileChange(this._schedule.calendar, this._schedule, now) : null;
761
+ const before = this.profile;
762
+ this._override = { profile, until: until === null ? Infinity : until };
763
+ this._reschedule(false);
764
+ this._profileChanged(before, "override");
765
+ }
766
+ // ── Lifecycle ──────────────────────────────────────────────────────────
767
+ connectedCallback() {
768
+ this._resizeObserver = new ResizeObserver(() => {
769
+ cancelAnimationFrame(this._measureFrame);
770
+ this._measureFrame = requestAnimationFrame(() => this._measure(false));
771
+ });
772
+ this._resizeObserver.observe(this);
773
+ this._mutationObserver = new MutationObserver((records) => {
774
+ if (!records.some((r) => this._concerns(r))) return;
775
+ this._cancelGesture();
776
+ this._sync(true);
777
+ });
778
+ this._mutationObserver.observe(this, { childList: true, subtree: true, attributeFilter: ["item", "size"] });
779
+ if (!this._explicit) this._reconcile();
780
+ this._measure(true);
781
+ this._syncControls();
782
+ if (this._editing) this._startEditing();
783
+ document.addEventListener("visibilitychange", this._onVisibility);
784
+ this._reschedule(true, "schedule", true);
785
+ }
786
+ disconnectedCallback() {
787
+ this._resizeObserver.disconnect();
788
+ cancelAnimationFrame(this._measureFrame);
789
+ this._mutationObserver.disconnect();
790
+ this._cancelGesture();
791
+ this._stopEditing();
792
+ this._hideToast(false);
793
+ document.removeEventListener("visibilitychange", this._onVisibility);
794
+ clearTimeout(this._profileTimer);
795
+ for (const timer of this._watching) clearTimeout(timer);
796
+ this._watching.clear();
797
+ this._watchedTags.clear();
798
+ if (this._orphanWatch) cancelAnimationFrame(this._orphanWatch);
799
+ this._orphanWatch = 0;
800
+ }
801
+ /** Whether a mutation is news: our own child list, or a change the grid didn't make itself. */
802
+ _concerns(r) {
803
+ if (r.type === "childList") return r.target === this;
804
+ const el = r.target;
805
+ if (el.parentElement !== this) return false;
806
+ if (r.attributeName === "size") return !this._explicit && el.getAttribute("size") !== this._written.get(el);
807
+ return el.getAttribute("item") !== this._itemOf.get(el);
808
+ }
809
+ attributeChangedCallback(name, old, value) {
810
+ if (name === "locked") {
811
+ if (this.locked) {
812
+ this.editing = false;
813
+ this._cancelGesture();
814
+ this._hideToast(false);
815
+ }
816
+ this._syncControls();
817
+ } else if (name === "edit-controls") {
818
+ this._syncControls();
819
+ } else if (name === "editing") {
820
+ const on = this.hasAttribute("editing");
821
+ if (on === this._editing) return;
822
+ if (on && this.locked) {
823
+ warnOnce("grid-locked-editing", TEXT.lockedWarning);
824
+ this.removeAttribute("editing");
825
+ return;
826
+ }
827
+ this._editing = on;
828
+ if (on) this._startEditing();
829
+ else this._stopEditing();
830
+ this._emit("ts-edit-change", { editing: on });
831
+ } else if (name === "profile") {
832
+ this._cancelGesture();
833
+ this._sync(false);
834
+ if (this._rendered && old !== value) this._announce(TEXT.profile(this.profile));
835
+ }
836
+ }
837
+ // ── Profiles on a schedule ─────────────────────────────────────────────
838
+ /** Work out the scheduled profile now and arm a timer for the next change. */
839
+ _reschedule(emit, reason = "schedule", quiet = false) {
840
+ clearTimeout(this._profileTimer);
841
+ const before = this.profile;
842
+ const s = this._schedule;
843
+ if (!s || !this.isConnected) {
844
+ this._scheduled = null;
845
+ this._deferred = false;
846
+ } else if ((this._editing || this._gesture) && this._scheduled !== null) {
847
+ this._deferred = true;
848
+ return;
849
+ } else {
850
+ this._deferred = false;
851
+ const now = this._clock();
852
+ if (this._override && now >= this._override.until) {
853
+ this._override = null;
854
+ reason = "override-ended";
855
+ }
856
+ this._scheduled = activeProfile(s.calendar, s, now);
857
+ const next = nextProfileChange(s.calendar, s, now);
858
+ const until = this._override ? Math.min(this._override.until, next === null ? Infinity : next) : next;
859
+ const wait = until === null || until === Infinity ? MAX_WAIT : Math.min(MAX_WAIT, Math.max(0, until - now));
860
+ this._profileTimer = setTimeout(() => this._reschedule(true), wait);
861
+ }
862
+ if (emit) this._profileChanged(before, reason, quiet);
863
+ }
864
+ _profileChanged(before, reason, quiet = false) {
865
+ const profile = this.profile;
866
+ if (profile === before) return;
867
+ this._cancelGesture();
868
+ this._sync(false);
869
+ if (!quiet) this._announce(TEXT.profile(profile));
870
+ this._emit("ts-profile-change", { profile, reason });
871
+ }
872
+ /** Apply a scheduled change that fell due while the user was editing or dragging. */
873
+ _applyDeferred() {
874
+ if (this._deferred && !this._editing && !this._gesture && this.isConnected) this._reschedule(true);
875
+ }
876
+ // ── Measuring and rendering ────────────────────────────────────────────
877
+ _measure(force) {
878
+ if (!this.isConnected) return;
879
+ const width = this.clientWidth;
880
+ if (!force && width === this._width) return;
881
+ this._width = width;
882
+ const columns = columnsFor(width);
883
+ const changed = columns !== this._columns;
884
+ if (changed && this._gesture) this._cancelGesture();
885
+ const style = getComputedStyle(this);
886
+ const compact = columns === 2;
887
+ const row = px(style.getPropertyValue(compact ? "--ts-row-compact" : "--ts-row"), 0) || px(style.getPropertyValue("--ts-row"), 150);
888
+ const gap = px(style.getPropertyValue(compact ? "--ts-gap-compact" : "--ts-gap"), compact ? 10 : 16);
889
+ this._columns = columns;
890
+ this._m = metrics(width, columns, row, gap);
891
+ if (force || changed || this._rendered) this._sync(false, !force);
892
+ const initial = !this._columnsSeen;
893
+ this._columnsSeen = true;
894
+ if (initial || changed) this._emit("ts-columns-change", { columns, device: columns === 2 ? "phone" : "desktop", initial });
895
+ }
896
+ _profileItems() {
897
+ const profiles = this._layout.profiles;
898
+ const name = this.profile;
899
+ return hasOwn(profiles, name) && Array.isArray(profiles[name]) ? profiles[name] : [];
900
+ }
901
+ _removedFor(profile) {
902
+ let set = this._removed.get(profile);
903
+ if (!set) this._removed.set(profile, set = /* @__PURE__ */ new Set());
904
+ return set;
905
+ }
906
+ /**
907
+ * Implicit mode: bring the active profile in line with the children. The
908
+ * first pass seeds it; later ones keep the grid's list as the truth, so a
909
+ * moved or resized tile stays put when a framework re-renders.
910
+ */
911
+ _reconcile() {
912
+ const kids = [...this.children].filter(isTile);
913
+ const profile = this.profile;
914
+ const prev = this._profileItems();
915
+ const removed = this._removedFor(profile);
916
+ const descriptors = kids.map((el) => {
917
+ if (!this._declared.has(el)) this._declared.set(el, el.getAttribute("size"));
918
+ return {
919
+ tag: el.localName,
920
+ item: el.getAttribute("item") || void 0,
921
+ declaredSize: this._declared.get(el) || void 0,
922
+ spec: elementSpec(el)
923
+ };
924
+ });
925
+ const current = itemsFromChildren(descriptors, (tag) => this._typeFor(tag), removed);
926
+ kids.forEach((el, i) => {
927
+ if (el.getAttribute("item") !== current[i].id) el.setAttribute("item", current[i].id);
928
+ });
929
+ this._warnGenerated(kids, descriptors, current);
930
+ this._matchChildren(false);
931
+ let items = reconcile(prev, current, removed);
932
+ const resized = hostResizes(kids.map((el, i) => ({ id: current[i].id, size: el.getAttribute("size"), written: this._written.get(el) })));
933
+ for (const [id, size] of resized) items = resizeItem(items, id, size, this._resolve);
934
+ kids.forEach((el) => {
935
+ if (resized.has(el.getAttribute("item"))) this._written.set(el, el.getAttribute("size"));
936
+ });
937
+ if (items.length !== prev.length || items.some((item, i) => item !== prev[i])) {
938
+ this._layout = withProfile(this._layout, profile, items);
939
+ }
940
+ }
941
+ /** The type of a child tag and the spec the catalogs give it. */
942
+ _typeFor(tag) {
943
+ var _a, _b;
944
+ const type = (_b = (_a = typeForTag(this._catalog, tag)) != null ? _a : typeForTag(WIDGETS, tag)) != null ? _b : tag;
945
+ return { type, spec: this._typeSpec({ id: type }) };
946
+ }
947
+ /** Match children to layout items, hide the rest, and lay out. */
948
+ _sync(animate, keepTiles) {
949
+ if (!this.isConnected) return;
950
+ if (!keepTiles && !this._explicit) this._reconcile();
951
+ if (!this._m) return;
952
+ const before = new Set(this._tiles.keys());
953
+ if (!keepTiles) this._matchChildren(true);
954
+ const items = this._profileItems();
955
+ this._order = items.filter((item) => this._tiles.has(item.id));
956
+ const shown = new Set(this._order.map((item) => item.id));
957
+ for (const [id, el] of this._tiles) if (!shown.has(id)) el.style.display = "none";
958
+ for (const id of this._chromes.keys()) if (!shown.has(id)) this._dropChrome(id);
959
+ this._render(animate);
960
+ if (animate && this._motion() && this._wasShown && this._wasShown.size) {
961
+ for (const id of shown) {
962
+ if (before.has(id) && this._wasShown.has(id)) continue;
963
+ this._tiles.get(id).animate([{ opacity: 0, scale: 0.92 }, { opacity: 1, scale: 1 }], { duration: 220, easing: "cubic-bezier(.2,.8,.2,1)" });
964
+ }
965
+ }
966
+ this._wasShown = shown;
967
+ this._rendered = true;
968
+ this._syncControls();
969
+ if (!keepTiles) this._watchOrphans();
970
+ }
971
+ /** Match children to items by their item attribute. `hide` also hides the ones that match nothing. */
972
+ _matchChildren(hide) {
973
+ this._tiles.clear();
974
+ for (const el of this.children) {
975
+ if (!isTile(el)) continue;
976
+ const id = el.getAttribute("item");
977
+ this._itemOf.set(el, id);
978
+ if (id && !this._tiles.has(id)) this._tiles.set(id, el);
979
+ else if (hide) {
980
+ el.style.display = "none";
981
+ if (!id && this._explicit) warnOnce(`grid-noitem:${el.localName}`, `<ts-grid> child <${el.localName}> has no item attribute, so no layout item can place it.`);
982
+ }
983
+ if (hide) this._watchDefined(el);
984
+ }
985
+ }
986
+ /** DEV: a dashed tag that is still undefined after a while is usually a missing import. */
987
+ _watchDefined(el) {
988
+ const tag = el.localName;
989
+ if (!tag.includes("-") || typeof customElements === "undefined" || customElements.get(tag) || this._watchedTags.has(tag)) return;
990
+ this._watchedTags.add(tag);
991
+ const timer = setTimeout(() => {
992
+ this._watching.delete(timer);
993
+ this._watchedTags.delete(tag);
994
+ if (!customElements.get(tag)) warnOnce(`grid-undefined:${tag}`, `<ts-grid> child <${tag}> is still undefined after 2 s. Did you import 'tickslate/elements'?`);
995
+ }, UNDEFINED_TAG_MS);
996
+ this._watching.add(timer);
997
+ }
998
+ /** DEV: a layout item whose child hasn't appeared after two frames is kept but not shown. */
999
+ _watchOrphans() {
1000
+ if (!this._explicit || this._orphanWatch || typeof requestAnimationFrame !== "function") return;
1001
+ let frames = 2;
1002
+ const step = () => {
1003
+ if (--frames > 0) {
1004
+ this._orphanWatch = requestAnimationFrame(step);
1005
+ return;
1006
+ }
1007
+ this._orphanWatch = 0;
1008
+ for (const item of this._profileItems()) {
1009
+ if (!this._tiles.has(item.id)) warnOnce(`grid-orphan:${item.id}`, `<ts-grid> layout item "${item.id}" has no child with item="${item.id}"; it is kept in the layout but not shown.`);
1010
+ }
1011
+ };
1012
+ this._orphanWatch = requestAnimationFrame(step);
1013
+ }
1014
+ /** DEV: generated ids for a repeated type are fragile across reorders. */
1015
+ _warnGenerated(kids, descriptors, current) {
1016
+ const byTag = /* @__PURE__ */ new Map();
1017
+ kids.forEach((el, i) => {
1018
+ const entry = byTag.get(el.localName) || { instances: 0, generated: [] };
1019
+ entry.instances++;
1020
+ if (!descriptors[i].item || descriptors[i].item !== current[i].id) entry.generated.push(current[i].id);
1021
+ byTag.set(el.localName, entry);
1022
+ });
1023
+ for (const [tag, { instances, generated }] of byTag) {
1024
+ if (instances >= 2 && generated.length) {
1025
+ warnOnce(`grid-generated:${tag}`, `<ts-grid> generated ids ${generated.join(", ")} for repeated <${tag}> children. Give each an item attribute so saved layouts survive reordering.`);
1026
+ }
1027
+ }
1028
+ }
1029
+ _render(animate, skipId) {
1030
+ const m = this._m;
1031
+ if (!m) return;
1032
+ animate = animate && this._motion();
1033
+ const { placements, rows } = place(this._order, this._columns, this._resolve);
1034
+ this._placements = new Map(placements.map((p) => [p.id, p]));
1035
+ for (const p of placements) {
1036
+ const box = boxOf(p, m);
1037
+ const el = this._tiles.get(p.id);
1038
+ const slide = animate && this._boxes.has(p.id) && el.style.display !== "none";
1039
+ this._boxes.set(p.id, box);
1040
+ if (el.getAttribute("size") !== p.variant) el.setAttribute("size", p.variant);
1041
+ this._written.set(el, p.variant);
1042
+ const chrome = this._chrome(p.id);
1043
+ if (p.id === skipId) continue;
1044
+ setBox(el, box, slide);
1045
+ setBox(chrome, box, slide);
1046
+ const held = this._gesture && this._gesture.id === p.id;
1047
+ if (this._editing && !held && !this._wiggles.has(p.id)) this._wiggle(p.id);
1048
+ if (this._editing) this._makeInert(el);
1049
+ }
1050
+ this._orderChromes(placements);
1051
+ this._board.style.transition = animate ? "" : "none";
1052
+ this._board.style.height = `${boardHeight(rows || 1, m)}px`;
1053
+ }
1054
+ _chrome(id) {
1055
+ let chrome = this._chromes.get(id);
1056
+ if (!chrome) {
1057
+ chrome = document.createElement("div");
1058
+ chrome.className = "chrome";
1059
+ chrome.dataset.id = id;
1060
+ chrome.innerHTML = '<button class="handle" type="button" aria-describedby="hint" aria-pressed="false"></button><button class="remove" type="button" tabindex="-1"><span aria-hidden="true">−</span></button><button class="resize" type="button" tabindex="-1"></button>';
1061
+ this._chromeLayer.appendChild(chrome);
1062
+ this._chromes.set(id, chrome);
1063
+ }
1064
+ const label = this._label(id);
1065
+ const item = this._item(id);
1066
+ const size = item ? item.size : "";
1067
+ const handle = chrome.querySelector(".handle");
1068
+ setAttr(handle, "aria-label", TEXT.handle(label, size, this._position(id), this._order.length));
1069
+ setAttr(chrome.querySelector(".remove"), "aria-label", TEXT.removeLabel(label));
1070
+ setAttr(chrome.querySelector(".resize"), "aria-label", TEXT.resizeLabel(label, size));
1071
+ handle.tabIndex = this._editing ? 0 : -1;
1072
+ if (this._editing) chrome.removeAttribute("aria-hidden");
1073
+ else chrome.setAttribute("aria-hidden", "true");
1074
+ const spec = this._spec(id);
1075
+ chrome.classList.toggle("fixed", !spec || spec.sizes.length < 2);
1076
+ return chrome;
1077
+ }
1078
+ /**
1079
+ * Keep the chromes in the DOM in placement order, so Tab follows what the
1080
+ * eye sees. moveBefore keeps focus and running animations; without it a
1081
+ * moved node loses both, so the focused handle is refocused and the wiggle restarted.
1082
+ */
1083
+ _orderChromes(placements) {
1084
+ const layer = this._chromeLayer;
1085
+ const want = [...placements].sort((a, b) => a.row - b.row || a.col - b.col).map((p) => this._chromes.get(p.id));
1086
+ const active = this.shadowRoot.activeElement;
1087
+ const moved = [];
1088
+ for (let i = 0; i < want.length; i++) {
1089
+ const at = layer.children[i];
1090
+ if (at === want[i]) continue;
1091
+ if (typeof layer.moveBefore === "function") layer.moveBefore(want[i], at || null);
1092
+ else layer.insertBefore(want[i], at || null);
1093
+ moved.push(want[i]);
1094
+ }
1095
+ if (!moved.length || typeof layer.moveBefore === "function") return;
1096
+ if (active && active.isConnected && this.shadowRoot.activeElement !== active) active.focus({ preventScroll: true });
1097
+ if (this._editing) {
1098
+ for (const chrome of moved) if (!(this._gesture && this._gesture.id === chrome.dataset.id)) this._wiggle(chrome.dataset.id);
1099
+ }
1100
+ }
1101
+ /** Tiles are inert while editing, so a widget's own buttons and links can't be reached; the host's own value comes back after. */
1102
+ _makeInert(el) {
1103
+ if (this._inertWas.has(el)) return;
1104
+ this._inertWas.set(el, el.hasAttribute("inert"));
1105
+ el.setAttribute("inert", "");
1106
+ }
1107
+ _restoreInert() {
1108
+ for (const [el, was] of this._inertWas) if (!was) el.removeAttribute("inert");
1109
+ this._inertWas.clear();
1110
+ }
1111
+ _dropChrome(id) {
1112
+ this._unwiggle(id);
1113
+ const chrome = this._chromes.get(id);
1114
+ if (chrome) chrome.remove();
1115
+ this._chromes.delete(id);
1116
+ }
1117
+ _item(id) {
1118
+ return this._order.find((item) => item.id === id);
1119
+ }
1120
+ _spec(id) {
1121
+ const item = this._item(id);
1122
+ return item ? this._resolve(item) : null;
1123
+ }
1124
+ _label(id) {
1125
+ return this._labelFor(this._tiles.get(id), this._spec(id), id);
1126
+ }
1127
+ _labelFor(el, spec, id = el && el.getAttribute("item")) {
1128
+ return el && (el.getAttribute("label") || el.getAttribute("aria-label")) || spec && spec.label || id;
1129
+ }
1130
+ _motion() {
1131
+ return !(this._motionQuery && this._motionQuery.matches);
1132
+ }
1133
+ // ── Edit mode ──────────────────────────────────────────────────────────
1134
+ _startEditing() {
1135
+ if (!this.isConnected) return;
1136
+ const inside = document.activeElement && document.activeElement !== this && this.contains(document.activeElement) ? document.activeElement : null;
1137
+ const focusId = inside && [...this._tiles].find(([, el]) => el.contains(inside));
1138
+ for (const el of this._tiles.values()) this._makeInert(el);
1139
+ for (const [id, chrome] of this._chromes) {
1140
+ chrome.querySelector(".handle").tabIndex = 0;
1141
+ chrome.removeAttribute("aria-hidden");
1142
+ this._wiggle(id);
1143
+ }
1144
+ if (focusId && this._chromes.has(focusId[0])) this._chromes.get(focusId[0]).querySelector(".handle").focus({ preventScroll: true });
1145
+ document.addEventListener("keydown", this._onDocKeyDown);
1146
+ document.addEventListener("pointerdown", this._onDocPointerDown, true);
1147
+ this._syncControls();
1148
+ this._announce(TEXT.editStart(this._order.length));
1149
+ }
1150
+ _stopEditing() {
1151
+ if (this._gesture) this._cancelGesture();
1152
+ this._restoreInert();
1153
+ this._returnFocus();
1154
+ this._closeMenu(false);
1155
+ for (const [id, chrome] of this._chromes) {
1156
+ chrome.querySelector(".handle").tabIndex = -1;
1157
+ chrome.setAttribute("aria-hidden", "true");
1158
+ this._unwiggle(id);
1159
+ }
1160
+ document.removeEventListener("keydown", this._onDocKeyDown);
1161
+ document.removeEventListener("pointerdown", this._onDocPointerDown, true);
1162
+ this._editOrigin = null;
1163
+ this._syncControls();
1164
+ if (this.isConnected) this._announce(TEXT.editDone);
1165
+ this._applyDeferred();
1166
+ }
1167
+ /** A press outside the grid ends editing; one in the pill or its list is inside, because the path names the grid. */
1168
+ _onDocPointerDown(e) {
1169
+ if (this._editing && isOutside(e.composedPath(), this)) this.editing = false;
1170
+ }
1171
+ /** Put focus back where editing began, but only if it is inside the grid and about to be lost. */
1172
+ _returnFocus() {
1173
+ if (!this.isConnected) return;
1174
+ const active = this.shadowRoot.activeElement || (document.activeElement !== this && this.contains(document.activeElement) ? document.activeElement : null);
1175
+ if (!active) return;
1176
+ const origin = this._editOrigin;
1177
+ let candidate = null;
1178
+ if (origin) {
1179
+ const button = origin === this._editBtn;
1180
+ if (button) origin.tabIndex = 0;
1181
+ candidate = {
1182
+ target: origin,
1183
+ connected: origin.isConnected,
1184
+ // The button is hidden by the opt-out or the lock; a tile is only focusable if it has a tab index.
1185
+ hidden: button ? !this._controlsOn() : origin.hidden || origin.tabIndex < 0 || origin.style.display === "none",
1186
+ disabled: !!origin.disabled
1187
+ };
1188
+ }
1189
+ this._focus(pickFocusTarget([candidate]) || this);
1190
+ }
1191
+ /** The grid itself is the last resort, so it gets a tab index and a name when the host gave none. */
1192
+ _focus(target) {
1193
+ if (target === this) {
1194
+ if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "-1");
1195
+ if (!this.hasAttribute("aria-label") && !this.hasAttribute("aria-labelledby")) this.setAttribute("aria-label", TEXT.dashboard);
1196
+ if (!this.hasAttribute("role")) this.setAttribute("role", "group");
1197
+ }
1198
+ target.focus();
1199
+ }
1200
+ _controlsOn() {
1201
+ return !this.locked && this.getAttribute("edit-controls") !== "off";
1202
+ }
1203
+ // ── Edit controls: the Edit layout button, the pill, Add back, the empty board ──
1204
+ _syncControls() {
1205
+ const on = this._controlsOn();
1206
+ const editing = this._editing;
1207
+ const pillOn = on && editing;
1208
+ this._editBtn.hidden = !on;
1209
+ this._editBtn.tabIndex = editing ? -1 : 0;
1210
+ this._pill.hidden = !pillOn;
1211
+ this._dock.classList.toggle("has-pill", pillOn);
1212
+ const empty = this._rendered && !!this._m && this._order.length === 0;
1213
+ this._emptyPanel.hidden = !empty;
1214
+ const hidden = this.hiddenItems;
1215
+ for (const [owner, show] of [["pill", pillOn && hidden.length > 0], ["empty", empty && on && hidden.length > 0]]) {
1216
+ const c = this._addBack[owner];
1217
+ c.add.hidden = !show;
1218
+ if (show) c.add.querySelector(".t").textContent = TEXT.addBack(hidden.length);
1219
+ if (!show && this._menuOwner === owner) this._closeMenu(false);
1220
+ }
1221
+ const sig = hidden.map((h) => `${h.id}
1222
+ ${h.label}`).join("\n\n");
1223
+ if (sig !== this._menuSig) {
1224
+ this._menuSig = sig;
1225
+ for (const { menu } of Object.values(this._addBack)) {
1226
+ menu.textContent = "";
1227
+ const heading = document.createElement("span");
1228
+ heading.className = "menu-h";
1229
+ heading.textContent = TEXT.hiddenHeading;
1230
+ menu.appendChild(heading);
1231
+ for (const h of hidden) {
1232
+ const item = document.createElement("button");
1233
+ item.type = "button";
1234
+ item.className = "mi";
1235
+ item.dataset.id = h.id;
1236
+ item.setAttribute("aria-label", TEXT.addBackItem(h.label));
1237
+ const name = document.createElement("span");
1238
+ name.textContent = h.label;
1239
+ const plus = document.createElement("span");
1240
+ plus.className = "plus";
1241
+ plus.setAttribute("aria-hidden", "true");
1242
+ plus.textContent = "+";
1243
+ item.append(name, plus);
1244
+ menu.appendChild(item);
1245
+ }
1246
+ }
1247
+ }
1248
+ }
1249
+ _toggleMenu(owner) {
1250
+ if (this._menuOwner === owner) return this._closeMenu(true);
1251
+ this._closeMenu(false);
1252
+ const c = this._addBack[owner];
1253
+ c.menu.hidden = false;
1254
+ c.add.setAttribute("aria-expanded", "true");
1255
+ this._menuOwner = owner;
1256
+ document.addEventListener("pointerdown", this._onMenuPointerDown, true);
1257
+ const first = c.menu.querySelector(".mi");
1258
+ if (first) first.focus();
1259
+ }
1260
+ /** `refocus` hands focus back to the Add back button, for Escape and a second press on it. */
1261
+ _closeMenu(refocus) {
1262
+ const owner = this._menuOwner;
1263
+ if (!owner) return;
1264
+ const c = this._addBack[owner];
1265
+ this._menuOwner = null;
1266
+ document.removeEventListener("pointerdown", this._onMenuPointerDown, true);
1267
+ c.menu.hidden = true;
1268
+ c.add.setAttribute("aria-expanded", "false");
1269
+ if (refocus && !c.add.hidden) c.add.focus();
1270
+ }
1271
+ _onMenuPointerDown(e) {
1272
+ if (this._menuOwner && isOutside(e.composedPath(), this._addBack[this._menuOwner].wrap)) this._closeMenu(false);
1273
+ }
1274
+ _restoreFromMenu(id) {
1275
+ const hidden = this.hiddenItems.find((h) => h.id === id);
1276
+ this._closeMenu(false);
1277
+ if (!hidden || !this.restore(id)) return;
1278
+ this._announce(TEXT.addedBack(hidden.label));
1279
+ const el = this._tiles.get(id);
1280
+ const chrome = this._chromes.get(id);
1281
+ this._focus(pickFocusTarget([
1282
+ chrome && { target: chrome.querySelector(".handle"), connected: chrome.isConnected, hidden: !this._editing },
1283
+ el && { target: el, connected: el.isConnected, hidden: el.tabIndex < 0 || el.style.display === "none" }
1284
+ ]) || this);
1285
+ }
1286
+ /**
1287
+ * A gentle rocking, run on the tile and its chrome with the same timing so
1288
+ * they move as one. The angle shrinks for big tiles so every tile's
1289
+ * corners travel about the same 2–3 px.
1290
+ */
1291
+ _wiggle(id) {
1292
+ if (!this._motion() || !this._editing) return;
1293
+ const el = this._tiles.get(id);
1294
+ const chrome = this._chromes.get(id);
1295
+ const box = this._boxes.get(id);
1296
+ if (!el || !chrome || !box) return;
1297
+ this._unwiggle(id);
1298
+ const deg = Math.min(0.9, 2.4 / (Math.max(box.w, box.h) / 2) * (180 / Math.PI));
1299
+ const index = this._order.findIndex((item) => item.id === id);
1300
+ const options = { duration: 130, iterations: Infinity, direction: "alternate", easing: "ease-in-out", delay: -(index * 47 % 260) };
1301
+ const frames = [{ rotate: `${-deg}deg` }, { rotate: `${deg}deg` }];
1302
+ this._wiggles.set(id, [el.animate(frames, options), chrome.animate(frames, options)]);
1303
+ }
1304
+ _unwiggle(id) {
1305
+ const running = this._wiggles.get(id);
1306
+ if (running) for (const a of running) a.cancel();
1307
+ this._wiggles.delete(id);
1308
+ }
1309
+ // ── Pointer gestures ───────────────────────────────────────────────────
1310
+ _hit(path) {
1311
+ const root = this.shadowRoot;
1312
+ for (const node of path) {
1313
+ if (node === this) break;
1314
+ if (node.nodeType !== 1) continue;
1315
+ if (node.getRootNode() === root) {
1316
+ if (node.classList.contains("remove")) return { kind: "remove", id: node.parentElement.dataset.id };
1317
+ if (node.classList.contains("resize")) return { kind: "resize", id: node.parentElement.dataset.id };
1318
+ if (node.classList.contains("handle")) return { kind: "chrome", id: node.parentElement.dataset.id };
1319
+ if (node === this._board) return { kind: "board" };
1320
+ } else if (node.parentElement === this && node.hasAttribute("item")) {
1321
+ const id = node.getAttribute("item");
1322
+ return this._placements.has(id) ? { kind: "tile", id } : { kind: "none" };
1323
+ }
1324
+ }
1325
+ return { kind: "none" };
1326
+ }
1327
+ _onPointerDown(e) {
1328
+ if (this.locked) return;
1329
+ if (this._gesture || !e.isPrimary || e.pointerType === "mouse" && e.button !== 0) return;
1330
+ const hit = this._hit(e.composedPath());
1331
+ const base = { pointerId: e.pointerId, pointerType: e.pointerType, startX: e.clientX, startY: e.clientY, x: e.clientX, y: e.clientY };
1332
+ if (!this._editing) {
1333
+ if (hit.kind !== "tile") return;
1334
+ this._begin({ ...base, kind: "press", id: hit.id });
1335
+ const g = this._gesture;
1336
+ g.squeeze = setTimeout(() => {
1337
+ if (this._motion()) g.squeezeAnim = this._tiles.get(g.id).animate([{ scale: 1 }, { scale: 0.97 }], { duration: LONG_PRESS_MS - 120, easing: "ease-out", fill: "forwards" });
1338
+ }, 120);
1339
+ g.timer = setTimeout(() => {
1340
+ this._unsqueeze(g);
1341
+ this._editOrigin = this._tiles.get(g.id);
1342
+ this.editing = true;
1343
+ this._lift(g);
1344
+ }, LONG_PRESS_MS);
1345
+ } else if (hit.kind === "chrome") {
1346
+ this._begin({ ...base, kind: "pending", id: hit.id });
1347
+ if (e.pointerType !== "mouse") {
1348
+ const g = this._gesture;
1349
+ g.timer = setTimeout(() => this._lift(g), TOUCH_HOLD_MS);
1350
+ }
1351
+ } else if (hit.kind === "resize") {
1352
+ e.preventDefault();
1353
+ this._begin({ ...base, kind: "resize", id: hit.id });
1354
+ this._startResize(this._gesture);
1355
+ } else if (hit.kind === "board") {
1356
+ this._begin({ ...base, kind: "tap" });
1357
+ }
1358
+ }
1359
+ _begin(g) {
1360
+ this._gesture = g;
1361
+ window.addEventListener("pointermove", this._onPointerMove);
1362
+ window.addEventListener("pointerup", this._onPointerUp);
1363
+ window.addEventListener("pointercancel", this._onPointerCancel);
1364
+ }
1365
+ _end() {
1366
+ const g = this._gesture;
1367
+ if (!g) return;
1368
+ clearTimeout(g.timer);
1369
+ clearTimeout(g.squeeze);
1370
+ clearTimeout(g.settle);
1371
+ cancelAnimationFrame(g.raf);
1372
+ this._unsqueeze(g);
1373
+ window.removeEventListener("pointermove", this._onPointerMove);
1374
+ window.removeEventListener("pointerup", this._onPointerUp);
1375
+ window.removeEventListener("pointercancel", this._onPointerCancel);
1376
+ this.removeAttribute("dragging");
1377
+ this._gesture = null;
1378
+ if (this._deferred && !this._editing) Promise.resolve().then(() => this._applyDeferred());
1379
+ }
1380
+ _unsqueeze(g) {
1381
+ clearTimeout(g.squeeze);
1382
+ if (g.squeezeAnim) g.squeezeAnim.cancel();
1383
+ g.squeezeAnim = null;
1384
+ }
1385
+ _onPointerMove(e) {
1386
+ const g = this._gesture;
1387
+ if (!g || e.pointerId !== g.pointerId) return;
1388
+ g.x = e.clientX;
1389
+ g.y = e.clientY;
1390
+ const moved = Math.hypot(g.x - g.startX, g.y - g.startY);
1391
+ if (g.kind === "press" || g.kind === "tap") {
1392
+ if (moved > PRESS_SLOP) this._end();
1393
+ } else if (g.kind === "pending") {
1394
+ if (g.pointerType === "mouse") {
1395
+ if (moved > MOUSE_DRAG_SLOP) this._lift(g);
1396
+ } else if (moved > PRESS_SLOP) {
1397
+ this._end();
1398
+ }
1399
+ } else if (g.kind === "resize") {
1400
+ this._resizeMove(g);
1401
+ }
1402
+ }
1403
+ _onPointerUp(e) {
1404
+ const g = this._gesture;
1405
+ if (!g || e.pointerId !== g.pointerId) return;
1406
+ if (g.kind === "drag") this._drop(g);
1407
+ else if (g.kind === "resize") this._endResize(g);
1408
+ else if (g.kind === "tap") {
1409
+ this._end();
1410
+ this.editing = false;
1411
+ } else this._end();
1412
+ }
1413
+ _onPointerCancel(e) {
1414
+ const g = this._gesture;
1415
+ if (g && e.pointerId === g.pointerId) this._cancelGesture();
1416
+ }
1417
+ // ── Dragging ───────────────────────────────────────────────────────────
1418
+ _lift(g) {
1419
+ if (this._gesture !== g) return;
1420
+ clearTimeout(g.timer);
1421
+ const el = this._tiles.get(g.id);
1422
+ const chrome = this._chromes.get(g.id);
1423
+ const box = this._boxes.get(g.id);
1424
+ if (!el || !chrome || !box) return this._end();
1425
+ const rect = this._board.getBoundingClientRect();
1426
+ g.kind = "drag";
1427
+ g.scroller = scrollParent(this);
1428
+ g.scroll0 = scrollPos(g.scroller);
1429
+ g.board0 = { left: rect.left, top: rect.top };
1430
+ g.view = g.scroller ? g.scroller.getBoundingClientRect() : { top: 0, bottom: innerHeight };
1431
+ g.offsetX = g.x - (rect.left + box.x);
1432
+ g.offsetY = g.y - (rect.top + box.y);
1433
+ g.from = this._order.slice();
1434
+ g.pending = -1;
1435
+ const item = this._item(g.id);
1436
+ const spec = this._spec(g.id);
1437
+ g.cols = footprint(item.size, this._columns, spec ? spec.sizes : null, spec ? spec.phoneRows : null).cols;
1438
+ this.setAttribute("dragging", "");
1439
+ if (g.pointerType !== "keyboard") {
1440
+ try {
1441
+ this.setPointerCapture(g.pointerId);
1442
+ } catch {
1443
+ }
1444
+ }
1445
+ if (g.pointerType !== "keyboard") this._suppressClick = true;
1446
+ this._unwiggle(g.id);
1447
+ liftStyle(el, chrome, true);
1448
+ if (g.pointerType === "keyboard") {
1449
+ chrome.querySelector(".handle").setAttribute("aria-pressed", "true");
1450
+ chrome.classList.add("held");
1451
+ }
1452
+ this._showPlaceholder(box, false);
1453
+ if (navigator.vibrate) navigator.vibrate(8);
1454
+ if (g.pointerType !== "keyboard") g.raf = requestAnimationFrame(this._frame);
1455
+ }
1456
+ _frame() {
1457
+ const g = this._gesture;
1458
+ if (!g || g.kind !== "drag") return;
1459
+ const now = performance.now();
1460
+ const dt = Math.min(50, now - (g.lastFrame || now)) / 1e3;
1461
+ g.lastFrame = now;
1462
+ const step = MAX_SCROLL * dt;
1463
+ if (g.y < g.view.top + EDGE) scrollBy(g.scroller, -step * Math.min(1, (g.view.top + EDGE - g.y) / EDGE));
1464
+ else if (g.y > g.view.bottom - EDGE) scrollBy(g.scroller, step * Math.min(1, (g.y - (g.view.bottom - EDGE)) / EDGE));
1465
+ const { x, y, target } = this._track(g);
1466
+ const translate = `${x}px ${y}px`;
1467
+ this._tiles.get(g.id).style.translate = translate;
1468
+ this._chromes.get(g.id).style.translate = translate;
1469
+ const current = this._order.findIndex((item) => item.id === g.id);
1470
+ if (target === current) {
1471
+ clearTimeout(g.settle);
1472
+ g.pending = -1;
1473
+ } else if (target !== g.pending) {
1474
+ g.pending = target;
1475
+ clearTimeout(g.settle);
1476
+ g.settle = setTimeout(() => this._reorder(g, g.pending), SETTLE_MS);
1477
+ }
1478
+ g.raf = requestAnimationFrame(this._frame);
1479
+ }
1480
+ /** Where the lifted tile is, relative to the board, and the slot under it. */
1481
+ _track(g) {
1482
+ const m = this._m;
1483
+ const scroll = scrollPos(g.scroller);
1484
+ const x = g.x - g.offsetX - (g.board0.left - (scroll.x - g.scroll0.x));
1485
+ const y = g.y - g.offsetY - (g.board0.top - (scroll.y - g.scroll0.y));
1486
+ const target = dropIndex(this._order, g.id, cellAt(x, y, g.cols, m), this._columns, this._resolve, m.pitchX / m.pitchY);
1487
+ return { x, y, target };
1488
+ }
1489
+ /** Take a new slot: the other tiles slide, the placeholder follows. */
1490
+ _reorder(g, index) {
1491
+ if (this._gesture !== g || index < 0) return;
1492
+ g.pending = -1;
1493
+ this._order = moveItem(this._order, g.id, index);
1494
+ this._render(true, g.kind === "drag" ? g.id : void 0);
1495
+ this._showPlaceholder(this._boxes.get(g.id), true);
1496
+ }
1497
+ _drop(g, placed) {
1498
+ clearTimeout(g.settle);
1499
+ if (g.pointerType !== "keyboard") {
1500
+ const { target } = this._track(g);
1501
+ if (target !== this._order.findIndex((item) => item.id === g.id)) this._reorder(g, target);
1502
+ }
1503
+ this._settle(g);
1504
+ const moved = !sameOrder(g.from, this._order);
1505
+ if (moved) this._commit("move", g.id);
1506
+ const label = this._label(g.id);
1507
+ const at = [this._position(g.id), this._order.length];
1508
+ if (g.pointerType !== "keyboard") {
1509
+ if (moved) this._announce(TEXT.moved(label, ...at));
1510
+ } else if (placed && moved) {
1511
+ this._announce(TEXT.droppedAt(label, ...at));
1512
+ } else {
1513
+ this._announce(TEXT.dropped(label));
1514
+ }
1515
+ }
1516
+ /** Glide the lifted tile into its slot and put it back among the others. */
1517
+ _settle(g) {
1518
+ const el = this._tiles.get(g.id);
1519
+ const chrome = this._chromes.get(g.id);
1520
+ const box = this._boxes.get(g.id);
1521
+ this._hidePlaceholder();
1522
+ this._end();
1523
+ setTimeout(() => {
1524
+ this._suppressClick = false;
1525
+ }, 0);
1526
+ if (chrome) {
1527
+ chrome.classList.remove("held");
1528
+ chrome.querySelector(".handle").setAttribute("aria-pressed", "false");
1529
+ }
1530
+ if (!el || !chrome || !box) return;
1531
+ const animate = this._motion();
1532
+ setBox(el, box, animate, true);
1533
+ setBox(chrome, box, animate, true);
1534
+ liftStyle(el, chrome, false, animate);
1535
+ const done = () => {
1536
+ if (this._gesture && this._gesture.id === g.id) return;
1537
+ el.style.zIndex = "";
1538
+ chrome.style.zIndex = "";
1539
+ chrome.classList.remove("lifted");
1540
+ if (this._editing) this._wiggle(g.id);
1541
+ };
1542
+ if (animate) setTimeout(done, 240);
1543
+ else done();
1544
+ }
1545
+ _cancelGesture() {
1546
+ const g = this._gesture;
1547
+ if (!g) return;
1548
+ if (g.kind === "drag" || g.kind === "resize") {
1549
+ clearTimeout(g.settle);
1550
+ this._order = g.from;
1551
+ this._render(true, g.id);
1552
+ this._settle(g);
1553
+ if (g.pointerType === "keyboard") this._announce(TEXT.cancelled(this._label(g.id)));
1554
+ } else {
1555
+ this._end();
1556
+ }
1557
+ }
1558
+ _showPlaceholder(box, animate) {
1559
+ const ph = this._placeholder;
1560
+ setBox(ph, box, animate && this._motion());
1561
+ ph.classList.add("on");
1562
+ }
1563
+ _hidePlaceholder() {
1564
+ this._placeholder.classList.remove("on");
1565
+ }
1566
+ // ── Resizing ───────────────────────────────────────────────────────────
1567
+ _startResize(g) {
1568
+ const box = this._boxes.get(g.id);
1569
+ const item = this._item(g.id);
1570
+ if (!box || !item) return this._end();
1571
+ g.box0 = box;
1572
+ g.from = this._order.slice();
1573
+ g.size0 = item.size;
1574
+ g.moved = false;
1575
+ this._unwiggle(g.id);
1576
+ this._chromes.get(g.id).classList.add("lifted");
1577
+ this._tiles.get(g.id).style.zIndex = "5";
1578
+ this._chromes.get(g.id).style.zIndex = "6";
1579
+ this._suppressClick = true;
1580
+ }
1581
+ _resizeMove(g) {
1582
+ const dx = g.x - g.startX;
1583
+ const dy = g.y - g.startY;
1584
+ if (!g.moved && Math.hypot(dx, dy) < MOUSE_DRAG_SLOP) return;
1585
+ if (!g.moved) {
1586
+ g.moved = true;
1587
+ this.setAttribute("dragging", "");
1588
+ }
1589
+ const spec = this._spec(g.id);
1590
+ if (!spec) return;
1591
+ const w = Math.max(this._m.colWidth, g.box0.w + dx);
1592
+ const h = Math.max(this._m.rowHeight, g.box0.h + dy);
1593
+ this._showPlaceholder({ x: g.box0.x, y: g.box0.y, w, h }, false);
1594
+ const extent2 = extentOf(w, h, this._m);
1595
+ const item = this._item(g.id);
1596
+ const size = sizeForExtent(spec, extent2.cols, extent2.rows, this._columns, item.size);
1597
+ if (size !== item.size) {
1598
+ this._order = resizeItem(this._order, g.id, size, this._resolve);
1599
+ this._render(true);
1600
+ }
1601
+ }
1602
+ _endResize(g) {
1603
+ const spec = this._spec(g.id);
1604
+ if (!g.moved && spec) {
1605
+ let next = stepSize(spec, g.size0, 1);
1606
+ if (next === g.size0) next = stepSize(spec, g.size0, -1) === g.size0 ? g.size0 : smallest(spec);
1607
+ this._order = resizeItem(this._order, g.id, next, this._resolve);
1608
+ this._render(true);
1609
+ }
1610
+ this._hidePlaceholder();
1611
+ this._end();
1612
+ setTimeout(() => {
1613
+ this._suppressClick = false;
1614
+ }, 0);
1615
+ const el = this._tiles.get(g.id);
1616
+ const chrome = this._chromes.get(g.id);
1617
+ setTimeout(() => {
1618
+ if (el) el.style.zIndex = "";
1619
+ if (chrome) {
1620
+ chrome.style.zIndex = "";
1621
+ chrome.classList.remove("lifted");
1622
+ }
1623
+ if (this._editing && this._placements.has(g.id)) this._wiggle(g.id);
1624
+ }, this._motion() ? 240 : 0);
1625
+ const size = this._item(g.id).size;
1626
+ if (size !== g.size0) {
1627
+ this._commit("resize", g.id);
1628
+ this._announce(TEXT.resized(this._label(g.id), size));
1629
+ }
1630
+ }
1631
+ // ── Removing ───────────────────────────────────────────────────────────
1632
+ _remove(id) {
1633
+ if (!this._editing || !this._placements.has(id) || this._gesture || this._removing.has(id)) return;
1634
+ const now = Date.now();
1635
+ if (now - this._removedAt < REMOVE_GUARD_MS) return;
1636
+ this._removedAt = now;
1637
+ const el = this._tiles.get(id);
1638
+ const chrome = this._chromes.get(id);
1639
+ const label = this._label(id);
1640
+ const ids = [...this._placements.values()].sort((a, b) => a.row - b.row || a.col - b.col).map((p) => p.id);
1641
+ const near = ids[ids.indexOf(id) + 1] || ids[ids.indexOf(id) - 1];
1642
+ this._removing.add(id);
1643
+ chrome.classList.add("removing");
1644
+ const finish = () => {
1645
+ if (!this._removing.delete(id)) return;
1646
+ const hadFocus = chrome.contains(this.shadowRoot.activeElement);
1647
+ this._order = removeItem(this._order, id);
1648
+ el.style.display = "none";
1649
+ this._dropChrome(id);
1650
+ this._placements.delete(id);
1651
+ this._render(true);
1652
+ const profile = this.profile;
1653
+ const previous = this._layout;
1654
+ const list = this._profileItems();
1655
+ const index = list.findIndex((item) => item.id === id);
1656
+ this._history.push(profile, previous);
1657
+ this._removedFor(profile).add(id);
1658
+ this._layout = withProfile(this._layout, profile, removeItem(list, id));
1659
+ this._emit("ts-layout-change", { layout: this._layout, items: this._profileItems(), previous, profile, reason: "remove", id, index, item: list[index] });
1660
+ this._syncControls();
1661
+ if (hadFocus) this._focusAfterRemove(near);
1662
+ if (!this._showToast(TEXT.removed(label))) this._announce(TEXT.removed(label) + ".");
1663
+ };
1664
+ if (!this._motion()) return finish();
1665
+ this._unwiggle(id);
1666
+ const out = [{ opacity: 1, scale: 1 }, { opacity: 0, scale: 0.85 }];
1667
+ const options = { duration: 180, easing: "ease-in", fill: "forwards" };
1668
+ chrome.animate(out, options);
1669
+ const done = () => {
1670
+ finish();
1671
+ for (const a of el.getAnimations()) a.cancel();
1672
+ };
1673
+ el.animate(out, options).finished.then(done, done);
1674
+ }
1675
+ /** Focus the removed tile's neighbour, else the pill's Done, else the grid, so it is never left on a node that is gone. */
1676
+ _focusAfterRemove(near) {
1677
+ const chrome = near && this._chromes.get(near);
1678
+ const done = this._pill.querySelector(".done");
1679
+ this._focus(pickFocusTarget([
1680
+ chrome && { target: chrome.querySelector(".handle"), connected: chrome.isConnected, hidden: !this._editing },
1681
+ { target: done, connected: done.isConnected, hidden: this._pill.hidden }
1682
+ ]) || this);
1683
+ }
1684
+ // ── The undo toast ─────────────────────────────────────────────────────
1685
+ /** Show the toast; false when the host turned it off, so the caller announces instead. */
1686
+ _showToast(text) {
1687
+ if (this.getAttribute("undo-toast") === "off") return false;
1688
+ const toast = this._toast;
1689
+ const wasOn = !!this._toastState;
1690
+ if (this._toastState) clearTimeout(this._toastState.timer);
1691
+ this._toastState = { remaining: TOAST_MS, started: 0, timer: 0, hover: false, focus: false };
1692
+ this._toastText.textContent = text;
1693
+ this._toastUndo.tabIndex = 0;
1694
+ toast.classList.add("on");
1695
+ if (!wasOn && this._motion() && toast.animate) toast.animate([{ translate: "0 12px", opacity: 0 }, { translate: "0 0", opacity: 1 }], { duration: 180, easing: "ease-out" });
1696
+ this._toastTimer();
1697
+ return true;
1698
+ }
1699
+ /** Start or pause the countdown: it runs only while neither hovered nor focused. */
1700
+ _toastTimer() {
1701
+ const t = this._toastState;
1702
+ if (!t) return;
1703
+ if (t.timer) {
1704
+ clearTimeout(t.timer);
1705
+ t.remaining = Math.max(0, t.remaining - (Date.now() - t.started));
1706
+ t.timer = 0;
1707
+ }
1708
+ if (t.hover || t.focus) return;
1709
+ t.started = Date.now();
1710
+ t.timer = setTimeout(() => this._hideToast(true), t.remaining);
1711
+ }
1712
+ _hideToast(animate) {
1713
+ const t = this._toastState;
1714
+ if (!t) return;
1715
+ clearTimeout(t.timer);
1716
+ this._toastState = null;
1717
+ const toast = this._toast;
1718
+ const off = () => {
1719
+ if (this._toastState) return;
1720
+ toast.classList.remove("on");
1721
+ this._toastText.textContent = "";
1722
+ this._toastUndo.tabIndex = -1;
1723
+ };
1724
+ if (this.shadowRoot.activeElement === this._toastUndo) this._toastUndo.blur();
1725
+ if (animate && this._motion() && toast.animate) toast.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 160, easing: "ease-in" }).finished.then(off, off);
1726
+ else off();
1727
+ }
1728
+ _undoFromToast() {
1729
+ const before = new Set(this._profileItems().map((item) => item.id));
1730
+ if (!this.undo()) return this._hideToast(false);
1731
+ const back = this._profileItems().find((item) => !before.has(item.id));
1732
+ const chrome = back && this._chromes.get(back.id);
1733
+ if (this._editing && chrome) chrome.querySelector(".handle").focus();
1734
+ }
1735
+ // ── Keyboard ───────────────────────────────────────────────────────────
1736
+ _onDocKeyDown(e) {
1737
+ if (e.defaultPrevented) return;
1738
+ if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && typeof e.key === "string" && e.key.toLowerCase() === "z") {
1739
+ const origin = e.composedPath()[0];
1740
+ if (origin && (origin.isContentEditable || /^(input|textarea|select)$/i.test(origin.localName || ""))) return;
1741
+ if (!this._gesture && this.undo()) {
1742
+ this._announce(TEXT.undone);
1743
+ e.preventDefault();
1744
+ }
1745
+ return;
1746
+ }
1747
+ if (e.key !== "Escape") return;
1748
+ if (this._gesture) this._cancelGesture();
1749
+ else {
1750
+ const active = document.activeElement;
1751
+ if (active && active !== document.body && !this.contains(active)) return;
1752
+ this.editing = false;
1753
+ }
1754
+ e.preventDefault();
1755
+ }
1756
+ /** The handle an event came from, if any. */
1757
+ _handleOf(e) {
1758
+ const el = e.composedPath()[0];
1759
+ return el && el.classList && el.classList.contains("handle") ? el : null;
1760
+ }
1761
+ /** The tile picked up and not yet dropped, by key or by tap; both share it. */
1762
+ _heldId() {
1763
+ const g = this._gesture;
1764
+ return g && g.kind === "drag" && g.pointerType === "keyboard" ? g.id : null;
1765
+ }
1766
+ _onKeyDown(e) {
1767
+ if (!this._editing || e.ctrlKey || e.metaKey || e.altKey) return;
1768
+ const handle = this._handleOf(e);
1769
+ if (!handle) return;
1770
+ const held = this._heldId();
1771
+ if (this._gesture && !held) return;
1772
+ const action = keyAction(e.key, handle.parentElement.dataset.id, { held });
1773
+ if (!action) return;
1774
+ e.preventDefault();
1775
+ this._perform(action);
1776
+ }
1777
+ /** A handle was activated without dragging: by tap, click, or a screen reader's activate. Null is the empty board. */
1778
+ _tap(id) {
1779
+ if (!this._editing || this.locked) return;
1780
+ const held = this._heldId();
1781
+ if (this._gesture && !held) return;
1782
+ const action = tapAction(id, { held });
1783
+ if (action) this._perform(action);
1784
+ }
1785
+ /** Run what keyAction or tapAction decided. */
1786
+ _perform(a) {
1787
+ const g = this._gesture;
1788
+ const id = a.id;
1789
+ if (a.type === "pickUp") {
1790
+ if (g || !this._placements.has(id)) return;
1791
+ this._gesture = { kind: "pending", pointerType: "keyboard", id, x: 0, y: 0 };
1792
+ this._lift(this._gesture);
1793
+ if (this._gesture) this._announce(TEXT.pickedUp(this._label(id)));
1794
+ } else if (a.type === "moveBy") {
1795
+ if (g && this._heldId() === id) this._keyMove(g, a.dCol, a.dRow);
1796
+ } else if (a.type === "placeAt") {
1797
+ if (g && this._heldId() === id) this._placeAt(g, a.targetId);
1798
+ } else if (a.type === "drop") {
1799
+ if (g && this._heldId() === id) this._drop(g);
1800
+ } else if (a.type === "cancel") {
1801
+ if (g && this._heldId() === id) this._cancelGesture();
1802
+ } else if (a.type === "resizeStep") {
1803
+ if (!g) this._resizeStep(id, a.dir);
1804
+ } else if (a.type === "remove") {
1805
+ if (!g) this._remove(id);
1806
+ }
1807
+ }
1808
+ /** Move the held tile to the target's slot and drop it there. */
1809
+ _placeAt(g, targetId) {
1810
+ const to = this._order.findIndex((item) => item.id === targetId);
1811
+ if (to < 0) return;
1812
+ if (to !== this._order.findIndex((item) => item.id === g.id)) {
1813
+ this._order = moveItem(this._order, g.id, to);
1814
+ this._render(true);
1815
+ }
1816
+ this._drop(g, true);
1817
+ this._reveal(g.id);
1818
+ }
1819
+ /** The resize button's click: a tap on it steps to the next size, wrapping, like the pointer's tap. */
1820
+ _resizeClick(id) {
1821
+ if (!this._editing || this._gesture) return;
1822
+ const spec = this._spec(id);
1823
+ const item = this._item(id);
1824
+ if (!spec || !item) return;
1825
+ let next = stepSize(spec, item.size, 1);
1826
+ if (next === item.size) next = stepSize(spec, item.size, -1) === item.size ? item.size : smallest(spec);
1827
+ this._applyResize(id, next);
1828
+ }
1829
+ _resizeStep(id, dir) {
1830
+ const spec = this._spec(id);
1831
+ const item = this._item(id);
1832
+ if (!spec || !item) return;
1833
+ this._applyResize(id, stepSize(spec, item.size, dir));
1834
+ }
1835
+ _applyResize(id, size) {
1836
+ const item = this._item(id);
1837
+ if (!item || size === item.size) return;
1838
+ this._order = resizeItem(this._order, id, size, this._resolve);
1839
+ this._render(true);
1840
+ this._commit("resize", id);
1841
+ this._announce(TEXT.resized(this._label(id), size));
1842
+ this._reveal(id);
1843
+ }
1844
+ /** Scroll the tile's handle into view once it has slid there, smoothly only when motion is welcome. */
1845
+ _reveal(id) {
1846
+ const show = () => {
1847
+ const chrome = this._chromes.get(id);
1848
+ const handle = chrome && chrome.querySelector(".handle");
1849
+ if (handle && handle.isConnected && handle.scrollIntoView) handle.scrollIntoView({ block: "nearest", behavior: this._motion() ? "smooth" : "auto" });
1850
+ };
1851
+ if (this._motion()) setTimeout(show, 240);
1852
+ else show();
1853
+ }
1854
+ /** Move a held tile one cell, going further if that cell can't hold it. */
1855
+ _keyMove(g, dCol, dRow) {
1856
+ const p = this._placements.get(g.id);
1857
+ const current = this._order.findIndex((item) => item.id === g.id);
1858
+ const limit = Math.max(this._columns, this._order.length * 2);
1859
+ for (let step = 1; step <= limit; step++) {
1860
+ const col = p.col + dCol * step;
1861
+ const row = p.row + dRow * step;
1862
+ if (col < 0 || row < 0 || col > this._columns - p.cols) break;
1863
+ const target = dropIndex(this._order, g.id, { col, row }, this._columns, this._resolve);
1864
+ if (target !== current) {
1865
+ this._order = moveItem(this._order, g.id, target);
1866
+ this._render(true);
1867
+ this._showPlaceholder(this._boxes.get(g.id), true);
1868
+ this._announce(TEXT.moved(this._label(g.id), this._position(g.id), this._order.length));
1869
+ this._reveal(g.id);
1870
+ return;
1871
+ }
1872
+ }
1873
+ this._announce(TEXT.blocked);
1874
+ }
1875
+ // ── Output ─────────────────────────────────────────────────────────────
1876
+ _commit(reason, id) {
1877
+ const previous = this._layout;
1878
+ this._history.push(this.profile, previous);
1879
+ const items = mergeOrder(this._profileItems(), this._order);
1880
+ this._layout = withProfile(this._layout, this.profile, items);
1881
+ this._emit("ts-layout-change", { layout: this._layout, items, previous, profile: this.profile, reason, id });
1882
+ }
1883
+ _position(id) {
1884
+ const sorted = [...this._placements.values()].sort((a, b) => a.row - b.row || a.col - b.col);
1885
+ return sorted.findIndex((p) => p.id === id) + 1;
1886
+ }
1887
+ /**
1888
+ * Speak `text` politely. The region is cleared first and filled on the next
1889
+ * frame, because setting identical text twice in a row says nothing the
1890
+ * second time. Texts queued in the same frame are read together, in order.
1891
+ */
1892
+ _announce(text) {
1893
+ const status = this._status;
1894
+ status.textContent = "";
1895
+ this._said.push(text);
1896
+ if (this._sayFrame) return;
1897
+ const say = () => {
1898
+ cancelAnimationFrame(this._sayFrame);
1899
+ clearTimeout(this._sayTimer);
1900
+ this._sayFrame = 0;
1901
+ status.textContent = this._said.join(" ");
1902
+ this._said = [];
1903
+ };
1904
+ this._sayFrame = requestAnimationFrame(say);
1905
+ this._sayTimer = setTimeout(say, 120);
1906
+ }
1907
+ _emit(name, detail) {
1908
+ this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
1909
+ }
1910
+ }
1911
+ function isTile(el) {
1912
+ return !NOT_TILES.has(el.localName) && el.getAttribute("slot") !== "empty";
1913
+ }
1914
+ function setBox(el, box, animate, lifted) {
1915
+ const s = el.style;
1916
+ s.position = "absolute";
1917
+ s.top = "0";
1918
+ s.left = "0";
1919
+ s.margin = "0";
1920
+ s.boxSizing = "border-box";
1921
+ s.width = `${box.w}px`;
1922
+ s.height = `${box.h}px`;
1923
+ s.transition = animate ? `translate ${SLIDE}, width ${SLIDE}, height ${SLIDE}${lifted ? `, scale ${SLIDE}, box-shadow ${SLIDE}` : ""}` : "none";
1924
+ s.translate = `${box.x}px ${box.y}px`;
1925
+ if (s.display === "none") s.display = "";
1926
+ }
1927
+ function liftStyle(el, chrome, on, animate = true) {
1928
+ if (on) {
1929
+ el.style.transition = "scale 160ms ease-out, box-shadow 160ms ease-out";
1930
+ chrome.style.transition = "scale 160ms ease-out";
1931
+ el.style.zIndex = "5";
1932
+ chrome.style.zIndex = "6";
1933
+ el.style.scale = String(LIFT_SCALE);
1934
+ chrome.style.scale = String(LIFT_SCALE);
1935
+ el.style.boxShadow = "var(--ts-shadow-lift, 0 24px 48px -16px rgba(0, 0, 0, .4))";
1936
+ el.style.borderRadius = "var(--ts-radius, 10px)";
1937
+ chrome.classList.add("lifted");
1938
+ } else {
1939
+ el.style.scale = "";
1940
+ chrome.style.scale = "";
1941
+ el.style.boxShadow = "";
1942
+ if (!animate) chrome.classList.remove("lifted");
1943
+ }
1944
+ }
1945
+ function setAttr(el, name, value) {
1946
+ if (el.getAttribute(name) !== value) el.setAttribute(name, value);
1947
+ }
1948
+ function sameOrder(a, b) {
1949
+ return a.length === b.length && a.every((item, i) => item.id === b[i].id);
1950
+ }
1951
+ function smallest(spec) {
1952
+ let size = spec.sizes[0];
1953
+ for (let next = stepSize(spec, size, -1); next !== size; next = stepSize(spec, size, -1)) size = next;
1954
+ return size;
1955
+ }
1956
+ function scrollParent(el) {
1957
+ for (let node = el.parentElement || el.getRootNode() && el.getRootNode().host; node; ) {
1958
+ if (node === document.body || node === document.documentElement) return null;
1959
+ const style = getComputedStyle(node);
1960
+ if (/(auto|scroll|overlay)/.test(style.overflowY) && node.scrollHeight > node.clientHeight) return node;
1961
+ node = node.parentElement || node.getRootNode() && node.getRootNode().host;
1962
+ }
1963
+ return null;
1964
+ }
1965
+ function scrollPos(scroller) {
1966
+ return scroller ? { x: scroller.scrollLeft, y: scroller.scrollTop } : { x: scrollX, y: scrollY };
1967
+ }
1968
+ function scrollBy(scroller, dy) {
1969
+ if (scroller) scroller.scrollTop += dy;
1970
+ else window.scrollBy(0, dy);
1971
+ }
1972
+ const TOKENS_CSS = `@layer tickslate.defaults {
1973
+ :where(:root) {
1974
+ --ts-bg: #F3EFE6;
1975
+ --ts-surface: #FFFDF8;
1976
+ --ts-surface-2: #F1EBDE;
1977
+ --ts-line: #E2DACB;
1978
+ --ts-ink: #1C1A16;
1979
+ --ts-muted: #6A655B;
1980
+ --ts-profit: #0E7A4E;
1981
+ --ts-loss: #C0362C;
1982
+ --ts-warn: #A85A06;
1983
+ --ts-accent: #2C52C4;
1984
+ --ts-axis: #948C7C;
1985
+ --ts-series-1: #2C52C4;
1986
+ --ts-series-2: #B45309;
1987
+ --ts-series-3: #0F766E;
1988
+ --ts-series-4: #8B3FB5;
1989
+ --ts-series-5: #BE185D;
1990
+ --ts-series-6: #6B7A1A;
1991
+ --ts-focus: var(--ts-accent);
1992
+ --ts-radius: 10px;
1993
+ --ts-radius-sm: 6px;
1994
+ --ts-shadow: 0 1px 0 rgba(28, 26, 22, .04), 0 8px 20px -12px rgba(28, 26, 22, .25);
1995
+ --ts-fill: 12%;
1996
+ --ts-font-ui: 'Geist', system-ui, -apple-system, 'Segoe UI', sans-serif;
1997
+ --ts-font-num: 'Newsreader', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
1998
+ --ts-row: 150px;
1999
+ --ts-gap: 16px;
2000
+ --ts-pad: 18px;
2001
+ --ts-row-compact: 150px;
2002
+ --ts-gap-compact: 10px;
2003
+ --ts-num-weight: 500;
2004
+ --ts-num-weight-small: 500;
2005
+ --ts-shadow-lift: 0 2px 6px rgba(28, 26, 22, .08), 0 28px 48px -18px rgba(28, 26, 22, .45);
2006
+ --ts-motion: 220ms;
2007
+ --ts-ease: cubic-bezier(.2, .8, .2, 1);
2008
+ }
2009
+ @media (prefers-color-scheme: dark) {
2010
+ :where([data-theme="auto"]) {
2011
+ --ts-bg: #111827;
2012
+ --ts-surface: #1F2937;
2013
+ --ts-surface-2: #374151;
2014
+ --ts-line: #374151;
2015
+ --ts-ink: #F3F4F6;
2016
+ --ts-muted: #9CA3AF;
2017
+ --ts-profit: #34D399;
2018
+ --ts-loss: #FB7185;
2019
+ --ts-warn: #FBBF24;
2020
+ --ts-accent: #818CF8;
2021
+ --ts-axis: #6B7280;
2022
+ --ts-series-1: #818CF8;
2023
+ --ts-series-2: #F59E0B;
2024
+ --ts-series-3: #2DD4BF;
2025
+ --ts-series-4: #C084FC;
2026
+ --ts-series-5: #F472B6;
2027
+ --ts-series-6: #A3E635;
2028
+ --ts-focus: var(--ts-accent);
2029
+ --ts-radius: 12px;
2030
+ --ts-pad: 16px;
2031
+ --ts-shadow: none;
2032
+ --ts-fill: 16%;
2033
+ --ts-num-weight-small: 600;
2034
+ --ts-shadow-lift: 0 2px 6px rgba(0, 0, 0, .3), 0 28px 48px -16px rgba(0, 0, 0, .7);
2035
+ }
2036
+ }
2037
+ :where([data-theme="dark"], .dark) {
2038
+ --ts-bg: #111827;
2039
+ --ts-surface: #1F2937;
2040
+ --ts-surface-2: #374151;
2041
+ --ts-line: #374151;
2042
+ --ts-ink: #F3F4F6;
2043
+ --ts-muted: #9CA3AF;
2044
+ --ts-profit: #34D399;
2045
+ --ts-loss: #FB7185;
2046
+ --ts-warn: #FBBF24;
2047
+ --ts-accent: #818CF8;
2048
+ --ts-axis: #6B7280;
2049
+ --ts-series-1: #818CF8;
2050
+ --ts-series-2: #F59E0B;
2051
+ --ts-series-3: #2DD4BF;
2052
+ --ts-series-4: #C084FC;
2053
+ --ts-series-5: #F472B6;
2054
+ --ts-series-6: #A3E635;
2055
+ --ts-focus: var(--ts-accent);
2056
+ --ts-radius: 12px;
2057
+ --ts-pad: 16px;
2058
+ --ts-shadow: none;
2059
+ --ts-fill: 16%;
2060
+ --ts-num-weight-small: 600;
2061
+ --ts-shadow-lift: 0 2px 6px rgba(0, 0, 0, .3), 0 28px 48px -16px rgba(0, 0, 0, .7);
2062
+ }
2063
+ :where([data-theme="light"]) {
2064
+ --ts-bg: #F3EFE6;
2065
+ --ts-surface: #FFFDF8;
2066
+ --ts-surface-2: #F1EBDE;
2067
+ --ts-line: #E2DACB;
2068
+ --ts-ink: #1C1A16;
2069
+ --ts-muted: #6A655B;
2070
+ --ts-profit: #0E7A4E;
2071
+ --ts-loss: #C0362C;
2072
+ --ts-warn: #A85A06;
2073
+ --ts-accent: #2C52C4;
2074
+ --ts-axis: #948C7C;
2075
+ --ts-series-1: #2C52C4;
2076
+ --ts-series-2: #B45309;
2077
+ --ts-series-3: #0F766E;
2078
+ --ts-series-4: #8B3FB5;
2079
+ --ts-series-5: #BE185D;
2080
+ --ts-series-6: #6B7A1A;
2081
+ --ts-focus: var(--ts-accent);
2082
+ --ts-radius: 10px;
2083
+ --ts-pad: 18px;
2084
+ --ts-shadow: 0 1px 0 rgba(28, 26, 22, .04), 0 8px 20px -12px rgba(28, 26, 22, .25);
2085
+ --ts-fill: 12%;
2086
+ --ts-num-weight-small: 500;
2087
+ --ts-shadow-lift: 0 2px 6px rgba(28, 26, 22, .08), 0 28px 48px -18px rgba(28, 26, 22, .45);
2088
+ }
2089
+ :where(ts-grid:not(:defined)) {
2090
+ display: block;
2091
+ min-height: var(--ts-row, 150px);
2092
+ }
2093
+ :where(ts-grid:not(:defined) > *) {
2094
+ display: none;
2095
+ }
2096
+ :where(:is(ts-today-pnl, ts-equity-curve, ts-session-clock, ts-pnl-calendar, ts-edge-stats, ts-capital-funding, ts-open-positions, ts-watchlist, ts-mini-chart, ts-card, ts-metric, ts-chart):not(:defined)) {
2097
+ display: block;
2098
+ height: var(--ts-row, 150px);
2099
+ }
2100
+ :where(:is(ts-pnl-calendar, ts-mini-chart):not(:defined):not([size])),
2101
+ :where(:is(ts-today-pnl, ts-equity-curve, ts-session-clock, ts-pnl-calendar, ts-edge-stats, ts-capital-funding, ts-open-positions, ts-watchlist, ts-mini-chart, ts-card, ts-metric, ts-chart):not(:defined):is([size="M"], [size="Tall"], [size="L"], [size="XL"])) {
2102
+ height: calc(2 * var(--ts-row, 150px) + var(--ts-gap, 16px));
2103
+ }
2104
+ }`;
2105
+ const checked = /* @__PURE__ */ new WeakSet();
2106
+ function ensureTokens(doc = typeof document === "undefined" ? void 0 : document) {
2107
+ if (!doc || !doc.documentElement || checked.has(doc)) return;
2108
+ checked.add(doc);
2109
+ const view = doc.defaultView || globalThis;
2110
+ let defined = "";
2111
+ try {
2112
+ defined = view.getComputedStyle(doc.documentElement).getPropertyValue("--ts-surface");
2113
+ } catch {
2114
+ }
2115
+ if (defined && defined.trim()) return;
2116
+ warnOnce("tokens", "tokens.css is not loaded, so tickslate injected its default tokens. Link tickslate/tokens.css (or set the --ts-* variables) to theme the widgets yourself.");
2117
+ adopt(doc, view);
2118
+ }
2119
+ function adopt(doc, view) {
2120
+ const Sheet = view.CSSStyleSheet;
2121
+ if (Array.isArray(doc.adoptedStyleSheets) && typeof Sheet === "function" && Sheet.prototype && "replaceSync" in Sheet.prototype) {
2122
+ try {
2123
+ const sheet = new Sheet();
2124
+ sheet.replaceSync(TOKENS_CSS);
2125
+ doc.adoptedStyleSheets = [...doc.adoptedStyleSheets, sheet];
2126
+ return;
2127
+ } catch {
2128
+ }
2129
+ }
2130
+ const style = doc.createElement("style");
2131
+ style.setAttribute("data-tickslate", "defaults");
2132
+ style.textContent = TOKENS_CSS;
2133
+ (doc.head || doc.documentElement).appendChild(style);
2134
+ }
2135
+ const STRINGS$6 = Object.freeze({
2136
+ noData: "No data",
2137
+ errorDefault: "Couldn't display this data",
2138
+ errorRegion: "{label}, error: {message}",
2139
+ retry: "Retry"
2140
+ });
2141
+ const Base = typeof HTMLElement === "undefined" ? class {
2142
+ } : HTMLElement;
2143
+ const FORMAT_ATTRS = [
2144
+ ["locale", "locale"],
2145
+ ["currency", "currency"],
2146
+ ["time-zone", "timeZone"]
2147
+ ];
2148
+ function resolveFormatOptions(...sources) {
2149
+ const out = {};
2150
+ for (const [attr, key] of FORMAT_ATTRS) {
2151
+ for (const source of sources) {
2152
+ const value = source && typeof source.getAttribute === "function" ? source.getAttribute(attr) : null;
2153
+ if (value) {
2154
+ out[key] = value;
2155
+ break;
2156
+ }
2157
+ }
2158
+ }
2159
+ return out;
2160
+ }
2161
+ function standaloneCss() {
2162
+ return SIZE_NAMES.map((name) => {
2163
+ const rows = SIZES[name].rows;
2164
+ const height = rows === 1 ? "var(--ts-row, 150px)" : `calc(${rows} * var(--ts-row, 150px) + ${rows - 1} * var(--ts-gap, 16px))`;
2165
+ return `:host([size="${name}"]) { height: ${height}; }`;
2166
+ }).join("\n");
2167
+ }
2168
+ function describeElement(el) {
2169
+ const item = el && typeof el.getAttribute === "function" ? el.getAttribute("item") : null;
2170
+ return `<${el.localName}${item ? ` item="${item}"` : ""}>`;
2171
+ }
2172
+ function escapeHtml(s) {
2173
+ return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
2174
+ }
2175
+ const SK_WIDTHS = Array.from({ length: 19 }, (_, i) => 10 + i * 5);
2176
+ const SK_HEIGHTS = [4, 6, 9, 10, 11, 12, 13, 16, 30, 44];
2177
+ const SK_CSS = SK_WIDTHS.map((w) => `.sk.w${w} { width: ${w}%; }`).join(" ") + " .sk.wx { width: 40px; } " + SK_HEIGHTS.map((h) => `.sk.h${h} { height: ${h}px; }`).join(" ");
2178
+ function sk(width, height) {
2179
+ const w = width === "x" ? "wx" : `w${SK_WIDTHS.reduce((a, b) => Math.abs(b - width) < Math.abs(a - width) ? b : a)}`;
2180
+ const h = SK_HEIGHTS.reduce((a, b) => Math.abs(b - height) < Math.abs(a - height) ? b : a);
2181
+ return `<span class="sk ${w} h${h}"></span>`;
2182
+ }
2183
+ const SHARED_CSS = `
2184
+ :host { display: block; }
2185
+ :host([hidden]) { display: none; }
2186
+ ${standaloneCss()}
2187
+ .tile {
2188
+ box-sizing: border-box; height: 100%; display: flex; flex-direction: column; gap: 10px;
2189
+ padding: var(--ts-pad, 18px); background: var(--ts-surface); border: 1px solid var(--ts-line);
2190
+ border-radius: var(--ts-radius); box-shadow: var(--ts-shadow); color: var(--ts-ink);
2191
+ font: 13px var(--ts-font-ui); overflow: hidden; position: relative;
2192
+ container-type: inline-size;
2193
+ }
2194
+ .hd { display: flex; align-items: center; gap: 8px; min-height: 18px; }
2195
+ .lb { font-size: 10px; text-transform: uppercase; letter-spacing: .12em; font-weight: 600; color: var(--ts-muted); }
2196
+ .n, .big, .xbig {
2197
+ font-family: var(--ts-font-num); font-variant-numeric: tabular-nums lining-nums; font-weight: var(--ts-num-weight, 500);
2198
+ }
2199
+ .big { font-size: min(34px, 19cqi); line-height: 1; letter-spacing: -.01em; }
2200
+ .xbig { font-size: min(48px, 9cqi); line-height: 1; letter-spacing: -.02em; }
2201
+ .md { font-size: min(17px, 12cqi); }
2202
+ .sm { font-size: 11px; }
2203
+ .xs { font-size: 10px; }
2204
+ .n.sm, .n.xs { font-weight: var(--ts-num-weight-small, 500); }
2205
+ .mut { color: var(--ts-muted); }
2206
+ .pos { color: var(--ts-profit); }
2207
+ .neg { color: var(--ts-loss); }
2208
+ .warn { color: var(--ts-warn); }
2209
+ .acc { color: var(--ts-accent); }
2210
+ .f1 { flex: 1; min-width: 0; }
2211
+ .row { display: flex; align-items: center; gap: 8px; }
2212
+ .rowb { display: flex; align-items: baseline; gap: 8px; }
2213
+ .col { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
2214
+ .g2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
2215
+ .g3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
2216
+ .g4 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
2217
+ .sep { border-top: 1px solid var(--ts-line); }
2218
+ .ell { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
2219
+ .btn {
2220
+ font: 500 11px var(--ts-font-ui); min-height: 28px; padding: 0 9px; border-radius: var(--ts-radius-sm);
2221
+ border: 1px solid var(--ts-line); background: transparent; color: var(--ts-muted); cursor: pointer;
2222
+ }
2223
+ .btn.on { background: var(--ts-surface-2); color: var(--ts-ink); box-shadow: inset 0 -2px 0 var(--ts-accent); }
2224
+ .btn:focus-visible { outline: 2px solid var(--ts-focus, var(--ts-accent)); outline-offset: 2px; }
2225
+ /* Touch screens: the 44px minimum target the RFC asks for at 2 columns. */
2226
+ @media (pointer: coarse) { .btn { min-height: 44px; min-width: 44px; } }
2227
+ .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex: none; }
2228
+ .dot.live { background: var(--ts-profit); animation: ts-pulse 2s infinite; }
2229
+ @keyframes ts-pulse {
2230
+ 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--ts-profit) 55%, transparent); }
2231
+ 70% { box-shadow: 0 0 0 7px transparent; }
2232
+ 100% { box-shadow: 0 0 0 0 transparent; }
2233
+ }
2234
+ @media (prefers-reduced-motion: reduce) { .dot.live { animation: none; } }
2235
+ .empty { flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; color: var(--ts-muted); text-align: center; }
2236
+ .err { flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; text-align: center; }
2237
+ .err-msg { display: inline-flex; align-items: flex-start; gap: 6px; max-width: 100%; font-size: 12px; font-weight: 500; color: var(--ts-ink); text-align: start; }
2238
+ .err-msg svg { width: 14px; height: 14px; flex: none; margin-top: 1px; color: var(--ts-warn); }
2239
+ .btn.retry { color: var(--ts-ink); }
2240
+ .sk { display: block; flex: none; position: relative; overflow: hidden; border-radius: var(--ts-radius-sm); background: var(--ts-surface-2); }
2241
+ .sk.grow { flex: 1; min-height: 0; width: 100%; }
2242
+ .sk::after {
2243
+ content: ''; position: absolute; inset: 0; transform: translateX(-100%);
2244
+ background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--ts-surface-2) 35%, var(--ts-surface)), transparent);
2245
+ animation: ts-shimmer 1.6s ease-in-out infinite;
2246
+ }
2247
+ @keyframes ts-shimmer { to { transform: translateX(100%); } }
2248
+ @media (prefers-reduced-motion: reduce) { .sk::after { animation: none; display: none; } }
2249
+ ${SK_CSS}
2250
+ .lp, .ln { fill: none; stroke-width: 2; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
2251
+ .lp { stroke: var(--ts-profit); }
2252
+ .ln { stroke: var(--ts-loss); }
2253
+ .ap { fill: color-mix(in srgb, var(--ts-profit) var(--ts-fill, 12%), transparent); }
2254
+ .an { fill: color-mix(in srgb, var(--ts-loss) var(--ts-fill, 12%), transparent); }
2255
+ .lz { stroke: var(--ts-axis); stroke-dasharray: 3 3; vector-effect: non-scaling-stroke; }
2256
+ `;
2257
+ const EMPTY_SPEC = Object.freeze({ label: "", sizes: Object.freeze([]) });
2258
+ const cssCache = /* @__PURE__ */ new WeakMap();
2259
+ const templateCache = /* @__PURE__ */ new WeakMap();
2260
+ const skeletonCache = /* @__PURE__ */ new WeakMap();
2261
+ function cssFor(Ctor) {
2262
+ let css = cssCache.get(Ctor);
2263
+ if (css === void 0) cssCache.set(Ctor, css = SHARED_CSS + (Ctor.styles || ""));
2264
+ return css;
2265
+ }
2266
+ function templateFor(Ctor, variant) {
2267
+ let byVariant = templateCache.get(Ctor);
2268
+ if (!byVariant) templateCache.set(Ctor, byVariant = /* @__PURE__ */ new Map());
2269
+ let html = byVariant.get(variant);
2270
+ if (html === void 0) byVariant.set(variant, html = Ctor.template(variant));
2271
+ return html;
2272
+ }
2273
+ function skeletonFor(Ctor, variant) {
2274
+ let byVariant = skeletonCache.get(Ctor);
2275
+ if (!byVariant) skeletonCache.set(Ctor, byVariant = /* @__PURE__ */ new Map());
2276
+ let html = byVariant.get(variant);
2277
+ if (html === void 0) byVariant.set(variant, html = Ctor.skeleton(variant));
2278
+ return html;
2279
+ }
2280
+ const WARN_GLYPH = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 1.9 15 14.1H1z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"></path><path d="M8 6.2v3.6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"></path><circle cx="8" cy="11.9" r=".95" fill="currentColor"></circle></svg>';
2281
+ const STATES = ["loading", "error"];
2282
+ let uids = 0;
2283
+ const NO_DATA_MS = 1e4;
2284
+ const FORMAT_ATTR = { locale: "locale", currency: "currency", timeZone: "time-zone" };
2285
+ function findGrid(el) {
2286
+ let node = el.parentElement || el.getRootNode && el.getRootNode().host;
2287
+ while (node) {
2288
+ if (node.localName === "ts-grid") return node;
2289
+ node = node.parentElement || node.getRootNode && node.getRootNode().host;
2290
+ }
2291
+ return null;
2292
+ }
2293
+ class TsWidget extends Base {
2294
+ static get observedAttributes() {
2295
+ return ["size", "label", "locale", "currency", "time-zone", "state", "data"];
2296
+ }
2297
+ constructor() {
2298
+ super();
2299
+ this._data = void 0;
2300
+ this._error = null;
2301
+ this._actions = Object.freeze([]);
2302
+ this._clock = null;
2303
+ this._queued = false;
2304
+ this._shown = null;
2305
+ this._refs = /* @__PURE__ */ new Map();
2306
+ this._fmt = null;
2307
+ this._fmtKey = "";
2308
+ this._every = 0;
2309
+ this._timer = 0;
2310
+ this._failed = false;
2311
+ this._dataSet = false;
2312
+ this._fromAttr = false;
2313
+ this._parseError = false;
2314
+ this._uid = ++uids;
2315
+ this._undefTimer = 0;
2316
+ this._checkedData = void 0;
2317
+ this._onVisibility = () => {
2318
+ if (this._visible()) {
2319
+ this._arm();
2320
+ this.requestRender();
2321
+ } else {
2322
+ this._disarm();
2323
+ }
2324
+ };
2325
+ const root = this.attachShadow({ mode: "open" });
2326
+ root.innerHTML = `<style>${cssFor(this.constructor)}</style><section class="tile" part="tile"></section>`;
2327
+ this._section = root.querySelector(".tile");
2328
+ this._section.addEventListener("click", (e) => {
2329
+ const target = e.target && e.target.closest ? e.target.closest("[data-action]") : null;
2330
+ if (target && target.getAttribute("data-action") === "retry" && this.actionEnabled("retry")) this.emit("retry");
2331
+ });
2332
+ }
2333
+ // ── Public API ─────────────────────────────────────────────────────────
2334
+ get data() {
2335
+ return this._data;
2336
+ }
2337
+ set data(value) {
2338
+ this._data = value;
2339
+ this._dataSet = true;
2340
+ this._fromAttr = false;
2341
+ this._parseError = false;
2342
+ this.requestRender();
2343
+ }
2344
+ /** 'loading', 'error' or null; anything else reads as null. */
2345
+ get state() {
2346
+ const value = this.getAttribute("state");
2347
+ return STATES.includes(value) ? value : null;
2348
+ }
2349
+ set state(value) {
2350
+ if (value === null || value === void 0 || value === "") this.removeAttribute("state");
2351
+ else this.setAttribute("state", value);
2352
+ }
2353
+ get error() {
2354
+ return this._error;
2355
+ }
2356
+ set error(value) {
2357
+ const message = value instanceof Error ? value.message : value;
2358
+ this._error = typeof message === "string" && message ? message : null;
2359
+ this.requestRender();
2360
+ }
2361
+ get actions() {
2362
+ return this._actions;
2363
+ }
2364
+ set actions(value) {
2365
+ this._actions = Object.freeze(Array.isArray(value) ? value.filter((a) => typeof a === "string") : []);
2366
+ this.requestRender();
2367
+ }
2368
+ actionEnabled(name) {
2369
+ return this._actions.includes(name);
2370
+ }
2371
+ get size() {
2372
+ const supported = this._supported();
2373
+ return snapSize(this.getAttribute("size") || supported[0], supported);
2374
+ }
2375
+ set size(value) {
2376
+ if (value === null || value === void 0 || value === "") this.removeAttribute("size");
2377
+ else this.setAttribute("size", value);
2378
+ }
2379
+ get variant() {
2380
+ return this.size;
2381
+ }
2382
+ get label() {
2383
+ const own = this.getAttribute("label");
2384
+ if (own) return own;
2385
+ return this.spec.label;
2386
+ }
2387
+ /**
2388
+ * The widget's description: catalog entry, else a validated static spec,
2389
+ * else nothing. Validated here, not at class definition, so a subclass can
2390
+ * set `static spec` in any order and a bad entry degrades to fewer sizes
2391
+ * rather than throwing in a render.
2392
+ */
2393
+ get spec() {
2394
+ const Ctor = this.constructor;
2395
+ const key = Ctor.catalogKey;
2396
+ if (key && Object.prototype.hasOwnProperty.call(WIDGETS, key)) return WIDGETS[key];
2397
+ const own = Ctor.spec;
2398
+ if (!own || typeof own !== "object") return EMPTY_SPEC;
2399
+ const out = {
2400
+ label: typeof own.label === "string" ? own.label : "",
2401
+ sizes: Array.isArray(own.sizes) ? own.sizes.filter(isSize) : []
2402
+ };
2403
+ if (own.phoneRows && typeof own.phoneRows === "object") out.phoneRows = own.phoneRows;
2404
+ return out;
2405
+ }
2406
+ set label(value) {
2407
+ if (value === null || value === void 0) this.removeAttribute("label");
2408
+ else this.setAttribute("label", value);
2409
+ }
2410
+ get clock() {
2411
+ return this._clock;
2412
+ }
2413
+ set clock(fn) {
2414
+ this._clock = typeof fn === "function" ? fn : null;
2415
+ this.requestRender();
2416
+ }
2417
+ now() {
2418
+ return this._clock ? this._clock() : Date.now();
2419
+ }
2420
+ get formatter() {
2421
+ const options = resolveFormatOptions(this, findGrid(this));
2422
+ const key = JSON.stringify([options, currentConfig()]);
2423
+ if (!this._fmt || key !== this._fmtKey) {
2424
+ this._fmt = createFormatter(options);
2425
+ this._fmtKey = key;
2426
+ }
2427
+ return this._fmt;
2428
+ }
2429
+ /** The first match for `selector` in the shadow root, remembered until the template is rebuilt. */
2430
+ $(selector) {
2431
+ let el = this._refs.get(selector);
2432
+ if (!el) {
2433
+ el = this.shadowRoot.querySelector(selector);
2434
+ if (el) this._refs.set(selector, el);
2435
+ }
2436
+ return el;
2437
+ }
2438
+ requestRender() {
2439
+ if (this._queued) return;
2440
+ this._queued = true;
2441
+ queueMicrotask(() => {
2442
+ this._queued = false;
2443
+ if (this.isConnected) this._render();
2444
+ });
2445
+ }
2446
+ every(ms) {
2447
+ const period = ms > 0 ? ms : 0;
2448
+ if (period === this._every && (this._timer || !period)) return;
2449
+ this._every = period;
2450
+ this._disarm();
2451
+ this._arm();
2452
+ }
2453
+ emit(action, detail) {
2454
+ return this.dispatchEvent(new CustomEvent("ts-action", { detail: { action, ...detail }, bubbles: true, composed: true, cancelable: true }));
2455
+ }
2456
+ /** Re-apply properties set on the element before it was defined, which would otherwise shadow the accessors. */
2457
+ upgrade(...names) {
2458
+ for (const name of names) {
2459
+ if (Object.prototype.hasOwnProperty.call(this, name)) {
2460
+ const value = this[name];
2461
+ delete this[name];
2462
+ this[name] = value;
2463
+ }
2464
+ }
2465
+ }
2466
+ // ── To override ────────────────────────────────────────────────────────
2467
+ static template() {
2468
+ return "";
2469
+ }
2470
+ static viewModel() {
2471
+ return {};
2472
+ }
2473
+ /** The default loading shape: a header, a headline bar and two text lines. */
2474
+ static skeleton() {
2475
+ return `<div class="hd"><span class="lb ell" data-k="label"></span></div>${sk(60, 30)}${sk(85, 11)}${sk(70, 11)}`;
2476
+ }
2477
+ viewContext() {
2478
+ return {};
2479
+ }
2480
+ update() {
2481
+ }
2482
+ // ── Lifecycle ──────────────────────────────────────────────────────────
2483
+ connectedCallback() {
2484
+ ensureTokens();
2485
+ this.upgrade("data", "size", "clock", "state", "error", "actions");
2486
+ this._normaliseSize();
2487
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", this._onVisibility);
2488
+ this._arm();
2489
+ this._watchForData();
2490
+ this.requestRender();
2491
+ }
2492
+ disconnectedCallback() {
2493
+ clearTimeout(this._undefTimer);
2494
+ if (typeof document !== "undefined") document.removeEventListener("visibilitychange", this._onVisibility);
2495
+ this._disarm();
2496
+ }
2497
+ attributeChangedCallback(name, oldValue, newValue) {
2498
+ if (oldValue === newValue) return;
2499
+ if (name === "size") this._normaliseSize();
2500
+ else if (name === "data") this._readDataAttribute(newValue);
2501
+ this.requestRender();
2502
+ }
2503
+ // ── Internals ──────────────────────────────────────────────────────────
2504
+ /**
2505
+ * Feed the data attribute into _data when no property has been set. A value
2506
+ * that isn't JSON-shaped is ignored, but data this attribute supplied earlier
2507
+ * is dropped so a removed attribute doesn't leave stale numbers behind.
2508
+ */
2509
+ _readDataAttribute(value) {
2510
+ if (this._dataSet) return;
2511
+ const text = typeof value === "string" ? value.trim() : "";
2512
+ if (text[0] !== "{" && text[0] !== "[") {
2513
+ if (this._fromAttr) {
2514
+ this._data = void 0;
2515
+ this._fromAttr = false;
2516
+ this._parseError = false;
2517
+ }
2518
+ return;
2519
+ }
2520
+ this._fromAttr = true;
2521
+ if (text === "[object Object]") {
2522
+ this._data = void 0;
2523
+ this._parseError = true;
2524
+ warnOnce(`attr-object:${this._uid}`, `${describeElement(this)} data attribute reads "[object Object]"; set the data property instead.`);
2525
+ return;
2526
+ }
2527
+ try {
2528
+ this._data = JSON.parse(text);
2529
+ this._parseError = false;
2530
+ } catch (err) {
2531
+ this._data = void 0;
2532
+ this._parseError = true;
2533
+ {
2534
+ const { message, position } = parseErrorParts(err);
2535
+ warnOnce(`attr-json:${this._uid}`, `${describeElement(this)} data attribute is not valid JSON: ${message}${position === null ? "" : ` (position ${position})`}.`);
2536
+ }
2537
+ }
2538
+ }
2539
+ // The checks below wrap their whole body in `if (DEV)`. A class method
2540
+ // survives tree-shaking even when its only caller is guarded, so the guard
2541
+ // has to be inside for a production build to lose the messages.
2542
+ /** Development: say so if nothing has set data (and nothing is loading on purpose) 10 s after connecting. */
2543
+ _watchForData() {
2544
+ {
2545
+ clearTimeout(this._undefTimer);
2546
+ if (this._data !== void 0 || this._parseError) return;
2547
+ this._undefTimer = setTimeout(() => {
2548
+ if (this._data === void 0 && !this._parseError && !this.state && !this.constructor.dataOptional && this.isConnected) {
2549
+ warnOnce(`no-data:${this._uid}`, `${describeElement(this)} has had no data for 10 s and still shows a skeleton. Set data, or null for the empty state.`);
2550
+ }
2551
+ }, NO_DATA_MS);
2552
+ if (this._undefTimer && typeof this._undefTimer.unref === "function") this._undefTimer.unref();
2553
+ }
2554
+ }
2555
+ /** Development: warn about invalid formatter options, naming where each came from. */
2556
+ _checkFormatter(fmt) {
2557
+ {
2558
+ for (const issue of fmt.issues) {
2559
+ const attr = FORMAT_ATTR[issue.option];
2560
+ const grid = findGrid(this);
2561
+ let source = "configure()";
2562
+ if (this.getAttribute(attr) === issue.value) source = `its ${attr} attribute`;
2563
+ else if (grid && grid.getAttribute(attr) === issue.value) source = `its grid's ${attr} attribute`;
2564
+ warnOnce(`format:${this._uid}:${issue.option}`, `${describeElement(this)} ${issue.message} (from ${source}); the default is used.`);
2565
+ }
2566
+ }
2567
+ }
2568
+ /** Development: a view model that couldn't read some rows says how many. */
2569
+ _checkDropped(vm) {
2570
+ if (vm.dropped > 0) {
2571
+ warnOnce(`dropped:${this._uid}`, `${describeElement(this)} dropped ${vm.dropped} ${vm.dropped === 1 ? "row" : "rows"} it couldn't read.`);
2572
+ }
2573
+ }
2574
+ /** Development: check data against the class's dataShape, once per new value. */
2575
+ _checkShape(data) {
2576
+ {
2577
+ const shape = this.constructor.dataShape;
2578
+ if (!shape || data === void 0 || this._checkedData === data) return;
2579
+ this._checkedData = data;
2580
+ const problem = checkShape(data, shape);
2581
+ if (problem) warnOnce(`shape:${this._uid}`, `${describeElement(this)} ${shapeMessage(problem)}`);
2582
+ }
2583
+ }
2584
+ _supported() {
2585
+ return this.spec.sizes;
2586
+ }
2587
+ /** The name for the tile: the label, else the tag, never empty. */
2588
+ _name() {
2589
+ const label = this.label;
2590
+ if (label) return label;
2591
+ {
2592
+ warnOnce(`label:${this.localName}`, `<${this.localName}> has no label, so its region is named "${this.localName}". Set a label attribute or static spec.`);
2593
+ }
2594
+ return this.localName;
2595
+ }
2596
+ /**
2597
+ * Make the attribute say the size actually rendered, so the standalone
2598
+ * height rules match it. Only once connected: a constructor or a detached
2599
+ * element must not grow attributes.
2600
+ */
2601
+ _normaliseSize() {
2602
+ if (!this.isConnected) return;
2603
+ const size = this.size;
2604
+ if (this.getAttribute("size") !== size) this.setAttribute("size", size);
2605
+ }
2606
+ _visible() {
2607
+ return typeof document === "undefined" || document.visibilityState !== "hidden";
2608
+ }
2609
+ _arm() {
2610
+ if (this._timer || !this._every || !this.isConnected || !this._visible()) return;
2611
+ this._timer = setInterval(() => this.requestRender(), this._every);
2612
+ }
2613
+ _disarm() {
2614
+ clearInterval(this._timer);
2615
+ this._timer = 0;
2616
+ }
2617
+ _render() {
2618
+ const Ctor = this.constructor;
2619
+ const label = this._name();
2620
+ const variant = this.variant;
2621
+ const state = this.state;
2622
+ if (state === "error" || this._error || this._parseError) {
2623
+ this._failed = false;
2624
+ return this._showError(label, this._error);
2625
+ }
2626
+ if (state === "loading" || this._data === void 0 && !Ctor.dataOptional) {
2627
+ this._failed = false;
2628
+ return this._showLoading(label, variant);
2629
+ }
2630
+ if (this._data === null) {
2631
+ this._failed = false;
2632
+ return this._showEmpty(label);
2633
+ }
2634
+ try {
2635
+ const fmt = this.formatter;
2636
+ if (DEV) {
2637
+ this._checkFormatter(fmt);
2638
+ this._checkShape(this._data);
2639
+ }
2640
+ const vm = Ctor.viewModel(this._data === void 0 ? {} : this._data, variant, { ...this.viewContext(), fmt, now: this.now(), label }) || {};
2641
+ if (DEV) this._checkDropped(vm);
2642
+ if (this._shown !== variant) {
2643
+ this._section.innerHTML = templateFor(Ctor, variant);
2644
+ this._refs.clear();
2645
+ this._shown = variant;
2646
+ }
2647
+ this._section.removeAttribute("aria-busy");
2648
+ this._section.setAttribute("aria-label", vm.ariaLabel || label);
2649
+ this.update(vm, variant);
2650
+ this._failed = false;
2651
+ } catch (err) {
2652
+ if (!this._failed) console.error(`tickslate: ${describeElement(this)} could not render its data; see docs/widgets/${this.localName}.md`, err);
2653
+ this._failed = true;
2654
+ this._showError(label, null);
2655
+ }
2656
+ }
2657
+ /** Swap in a non-content view's markup, and write the real label into it. */
2658
+ _show(key, html, label, name, busy) {
2659
+ if (this._shown !== key) {
2660
+ this._section.innerHTML = html;
2661
+ this._refs.clear();
2662
+ this._shown = key;
2663
+ }
2664
+ const slot = this._section.querySelector('[data-k="label"]');
2665
+ if (slot) slot.textContent = label;
2666
+ this._section.setAttribute("aria-label", name);
2667
+ if (busy) this._section.setAttribute("aria-busy", "true");
2668
+ else this._section.removeAttribute("aria-busy");
2669
+ }
2670
+ _showLoading(label, variant) {
2671
+ this._show(`view:loading:${variant}`, skeletonFor(this.constructor, variant), label, label, true);
2672
+ }
2673
+ _showEmpty(label) {
2674
+ const text = escapeHtml(this.constructor.emptyText);
2675
+ this._show("view:empty", `<div class="hd"><span class="lb ell" data-k="label"></span></div><div class="empty"><slot name="empty">${text}</slot></div>`, label, label, false);
2676
+ }
2677
+ _showError(label, message) {
2678
+ const text = message || STRINGS$6.errorDefault;
2679
+ const retry = this.actionEnabled("retry") ? `<button type="button" class="btn retry" data-action="retry">${escapeHtml(STRINGS$6.retry)}</button>` : "";
2680
+ this._show(
2681
+ retry ? "view:error:retry" : "view:error",
2682
+ `<div class="hd"><span class="lb ell" data-k="label"></span></div><div class="err" part="error"><span class="err-msg">${WARN_GLYPH}<span data-k="message"></span></span>${retry}</div>`,
2683
+ label,
2684
+ STRINGS$6.errorRegion.replace("{label}", label).replace("{message}", text),
2685
+ false
2686
+ );
2687
+ this._section.querySelector('[data-k="message"]').textContent = text;
2688
+ }
2689
+ }
2690
+ TsWidget.catalogKey = null;
2691
+ TsWidget.spec = null;
2692
+ TsWidget.styles = "";
2693
+ TsWidget.dataOptional = false;
2694
+ TsWidget.dataShape = null;
2695
+ TsWidget.emptyText = STRINGS$6.noData;
2696
+ const STRINGS$5 = Object.freeze({ synced: "synced " });
2697
+ const TEMPLATE = `
2698
+ <div class="hd">
2699
+ <span class="dot live" data-k="dot" hidden></span>
2700
+ <span class="lb ell" data-k="label"></span>
2701
+ <span class="f1"></span>
2702
+ <span class="sm mut n meta" data-k="meta" hidden><span class="pre">${STRINGS$5.synced}</span><span data-k="time"></span></span>
2703
+ <slot name="actions"></slot>
2704
+ </div>
2705
+ <slot name="value"></slot>
2706
+ <div class="body" data-k="body" part="body"><slot></slot></div>
2707
+ <slot name="footer"></slot>`;
2708
+ const SKELETON = `
2709
+ <div class="hd"><span class="lb ell" data-k="label"></span><span class="f1"></span><slot name="actions"></slot></div>
2710
+ <slot name="value"></slot>
2711
+ <span class="sk grow"></span>
2712
+ <slot name="footer"></slot>`;
2713
+ function cardSizes(attr) {
2714
+ const wanted = new Set(String(attr || "").split(/\s+/).filter(isSize));
2715
+ const sizes = SIZE_NAMES.filter((name) => wanted.has(name));
2716
+ return sizes.length ? sizes : [...SIZE_NAMES];
2717
+ }
2718
+ function cardModel(data, variant, ctx) {
2719
+ const d = data && typeof data === "object" ? data : {};
2720
+ const t = toTime(d.asOf !== void 0 ? d.asOf : ctx.asOf);
2721
+ const time = Number.isFinite(t) ? ctx.fmt.time(t) : "";
2722
+ return {
2723
+ label: ctx.label,
2724
+ ariaLabel: ctx.valueText ? `${ctx.label}, ${ctx.valueText}` : ctx.label,
2725
+ live: typeof d.live === "boolean" ? d.live : ctx.live === true,
2726
+ time,
2727
+ scrollable: ctx.scrollable === true
2728
+ };
2729
+ }
2730
+ const flag = (el, name, on) => on ? el.setAttribute(name, "") : el.removeAttribute(name);
2731
+ class TsCard extends TsWidget {
2732
+ static get observedAttributes() {
2733
+ return [...super.observedAttributes, "sizes", "live", "as-of", "scrollable"];
2734
+ }
2735
+ constructor() {
2736
+ super();
2737
+ this._specKey = null;
2738
+ this._spec = null;
2739
+ this._seen = null;
2740
+ this._section.addEventListener("scroll", () => this._fade(), true);
2741
+ }
2742
+ /** From the sizes and label attributes (RFC 0002 §3.1). Cached per attribute pair, as a grid asks often. */
2743
+ get spec() {
2744
+ const sizes = this.getAttribute("sizes");
2745
+ const label = this.getAttribute("label") || "";
2746
+ const key = `${sizes}
2747
+ ${label}`;
2748
+ if (key !== this._specKey) {
2749
+ this._specKey = key;
2750
+ this._spec = { label, sizes: cardSizes(sizes) };
2751
+ }
2752
+ return this._spec;
2753
+ }
2754
+ static template() {
2755
+ return TEMPLATE;
2756
+ }
2757
+ static skeleton() {
2758
+ return SKELETON;
2759
+ }
2760
+ static viewModel(data, variant, ctx) {
2761
+ return cardModel(data, variant, ctx);
2762
+ }
2763
+ viewContext() {
2764
+ return {
2765
+ live: this.hasAttribute("live"),
2766
+ asOf: this.getAttribute("as-of"),
2767
+ scrollable: this.hasAttribute("scrollable"),
2768
+ valueText: this._slottedText("value")
2769
+ };
2770
+ }
2771
+ connectedCallback() {
2772
+ super.connectedCallback();
2773
+ if (!this._seen && typeof MutationObserver !== "undefined") {
2774
+ this._seen = new MutationObserver(() => this.requestRender());
2775
+ this._seen.observe(this, { childList: true, subtree: true, characterData: true });
2776
+ }
2777
+ if (!this._resize && typeof ResizeObserver !== "undefined") {
2778
+ this._resize = new ResizeObserver(() => this._fade());
2779
+ this._resize.observe(this);
2780
+ }
2781
+ }
2782
+ disconnectedCallback() {
2783
+ super.disconnectedCallback();
2784
+ if (this._seen) this._seen.disconnect();
2785
+ if (this._resize) this._resize.disconnect();
2786
+ this._seen = null;
2787
+ this._resize = null;
2788
+ }
2789
+ attributeChangedCallback(name, oldValue, newValue) {
2790
+ if (oldValue === newValue) return;
2791
+ if (name === "sizes") this._normaliseSize();
2792
+ super.attributeChangedCallback(name, oldValue, newValue);
2793
+ }
2794
+ /** The base shows the empty view for null data; a card with a body shows the body. */
2795
+ _render() {
2796
+ const held = this._data;
2797
+ if (held === null && this._hasBody()) this._data = void 0;
2798
+ try {
2799
+ super._render();
2800
+ } finally {
2801
+ this._data = held;
2802
+ }
2803
+ }
2804
+ /** Anything slotted into the default slot: an element with no slot name, or text that isn't whitespace. */
2805
+ _hasBody() {
2806
+ for (const node of this.childNodes || []) {
2807
+ if (node.nodeType === 1) {
2808
+ if (!node.getAttribute("slot")) return true;
2809
+ } else if (node.nodeType === 3 && /\S/.test(node.textContent)) return true;
2810
+ }
2811
+ return false;
2812
+ }
2813
+ _slottedText(name) {
2814
+ for (const node of this.childNodes || []) {
2815
+ if (node.nodeType === 1 && node.getAttribute("slot") === name) return (node.textContent || "").trim();
2816
+ }
2817
+ return "";
2818
+ }
2819
+ update(vm) {
2820
+ const text = (k2, s) => {
2821
+ const el = this.$(`[data-k="${k2}"]`);
2822
+ if (el && el.textContent !== s) el.textContent = s;
2823
+ };
2824
+ text("label", vm.label);
2825
+ flag(this.$('[data-k="dot"]'), "hidden", !vm.live);
2826
+ flag(this.$('[data-k="meta"]'), "hidden", !vm.time);
2827
+ text("time", vm.time);
2828
+ const body = this.$('[data-k="body"]');
2829
+ body.setAttribute("class", vm.scrollable ? "body scroll" : "body");
2830
+ if (vm.scrollable) {
2831
+ body.setAttribute("tabindex", "0");
2832
+ body.setAttribute("role", "group");
2833
+ body.setAttribute("aria-label", this.label || this.localName);
2834
+ } else {
2835
+ body.removeAttribute("tabindex");
2836
+ body.removeAttribute("role");
2837
+ body.removeAttribute("aria-label");
2838
+ body.removeAttribute("data-more");
2839
+ }
2840
+ this._fade();
2841
+ }
2842
+ /** Show the fade only while the body has more to scroll to. */
2843
+ _fade() {
2844
+ const body = this.shadowRoot && this.$('[data-k="body"]');
2845
+ if (!body || typeof body.scrollHeight !== "number" || !/\bscroll\b/.test(body.getAttribute("class") || "")) return;
2846
+ flag(body, "data-more", body.scrollTop + body.clientHeight < body.scrollHeight - 1);
2847
+ }
2848
+ }
2849
+ __publicField(TsCard, "catalogKey", "card");
2850
+ __publicField(TsCard, "dataOptional", true);
2851
+ __publicField(TsCard, "dataShape", Object.freeze({
2852
+ asOf: Object.freeze({ kind: "time", optional: true }),
2853
+ live: Object.freeze({ kind: "boolean", optional: true })
2854
+ }));
2855
+ __publicField(TsCard, "styles", `
2856
+ [hidden] { display: none; }
2857
+ .body { flex: 1; min-height: 0; overflow: hidden; }
2858
+ .body.scroll { overflow: auto; scrollbar-width: thin; }
2859
+ /* The last 28px fade while there is more below, so a cut-off row reads as
2860
+ more to come; at the end of the list the fade goes, so nothing is dimmed
2861
+ that has nowhere left to scroll. */
2862
+ .body.scroll[data-more] {
2863
+ -webkit-mask-image: linear-gradient(to bottom, var(--ts-ink) calc(100% - 28px), transparent);
2864
+ mask-image: linear-gradient(to bottom, var(--ts-ink) calc(100% - 28px), transparent);
2865
+ }
2866
+ .body.scroll:focus-visible { outline: 2px solid var(--ts-focus, var(--ts-accent)); outline-offset: 2px; }
2867
+ /* A canvas or svg is inline by default, and its baseline gap would push a
2868
+ height: 100% chart a few pixels past the body. */
2869
+ ::slotted(canvas), ::slotted(svg), ::slotted(img), ::slotted(video), ::slotted(iframe) { display: block; }
2870
+ slot[name="value"]::slotted(*) {
2871
+ display: block; flex: none; white-space: nowrap; overflow: hidden; unicode-bidi: isolate;
2872
+ font-family: var(--ts-font-num); font-variant-numeric: tabular-nums lining-nums; font-weight: var(--ts-num-weight, 500);
2873
+ font-size: min(34px, 19cqi); line-height: 1; letter-spacing: -.01em;
2874
+ }
2875
+ slot[name="footer"]::slotted(*) {
2876
+ display: block; flex: none; font-size: 11px; color: var(--ts-muted);
2877
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
2878
+ }
2879
+ /* On a tile narrower than 200px the meta says '14:05', so the label keeps its room. */
2880
+ @container (max-width: 200px) { .pre { display: none; } }
2881
+ `);
2882
+ const round$3 = (v) => Math.round(v * 10) / 10;
2883
+ const finite = (p) => p && Number.isFinite(p.x) && Number.isFinite(p.y);
2884
+ function scale(d0, d1, r0, r1) {
2885
+ if (d0 === d1) return () => (r0 + r1) / 2;
2886
+ const k2 = (r1 - r0) / (d1 - d0);
2887
+ return (v) => r0 + (v - d0) * k2;
2888
+ }
2889
+ const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
2890
+ function extent(points, key) {
2891
+ let lo = Infinity;
2892
+ let hi = -Infinity;
2893
+ for (const p of points) {
2894
+ lo = Math.min(lo, p[key]);
2895
+ hi = Math.max(hi, p[key]);
2896
+ }
2897
+ return [lo, hi];
2898
+ }
2899
+ function linePath(points, x, y) {
2900
+ let d = "";
2901
+ for (const p of points) {
2902
+ if (!finite(p)) continue;
2903
+ d += `${d ? " L" : "M"}${round$3(x(p.x))} ${round$3(y(p.y))}`;
2904
+ }
2905
+ return d;
2906
+ }
2907
+ function linePaths(points, { width, height, pad = 4, xDomain, yDomain, includeZero = true } = {}) {
2908
+ const pts = (points || []).filter(finite);
2909
+ const [x0, x1] = xDomain || (pts.length ? extent(pts, "x") : [0, 1]);
2910
+ let [y0, y1] = yDomain || (pts.length ? extent(pts, "y") : [0, 1]);
2911
+ if (includeZero) {
2912
+ y0 = Math.min(y0, 0);
2913
+ y1 = Math.max(y1, 0);
2914
+ }
2915
+ const x = scale(x0, x1, pad, width - pad);
2916
+ const y = scale(y0, y1, height - pad, pad);
2917
+ const zeroY = round$3(clamp(y(0), pad, height - pad));
2918
+ if (!pts.length) return { line: "", area: "", zeroY, x, y };
2919
+ let line;
2920
+ let firstX;
2921
+ let lastX;
2922
+ if (pts.length === 1) {
2923
+ firstX = round$3(pad);
2924
+ lastX = round$3(width - pad);
2925
+ const py = round$3(y(pts[0].y));
2926
+ line = `M${firstX} ${py} L${lastX} ${py}`;
2927
+ } else {
2928
+ line = linePath(pts, x, y);
2929
+ firstX = round$3(x(pts[0].x));
2930
+ lastX = round$3(x(pts[pts.length - 1].x));
2931
+ }
2932
+ const area = `${line} L${lastX} ${zeroY} L${firstX} ${zeroY} Z`;
2933
+ return { line, area, zeroY, x, y };
2934
+ }
2935
+ function bandPath(lower, upper, x, y) {
2936
+ const top = (upper || []).filter(finite);
2937
+ const bottom = (lower || []).filter(finite);
2938
+ if (!top.length || !bottom.length) return "";
2939
+ const forward = linePath(top, x, y);
2940
+ let back = "";
2941
+ for (let i = bottom.length - 1; i >= 0; i--) back += ` L${round$3(x(bottom[i].x))} ${round$3(y(bottom[i].y))}`;
2942
+ return `${forward}${back} Z`;
2943
+ }
2944
+ function seriesDomain(series, { includeZero = true } = {}) {
2945
+ let x0 = Infinity;
2946
+ let x1 = -Infinity;
2947
+ let y0 = Infinity;
2948
+ let y1 = -Infinity;
2949
+ for (const points of series || []) {
2950
+ for (const p of points || []) {
2951
+ if (!finite(p)) continue;
2952
+ x0 = Math.min(x0, p.x);
2953
+ x1 = Math.max(x1, p.x);
2954
+ y0 = Math.min(y0, p.y);
2955
+ y1 = Math.max(y1, p.y);
2956
+ }
2957
+ }
2958
+ if (x0 === Infinity) return { x: [0, 1], y: [0, 1] };
2959
+ if (includeZero) {
2960
+ y0 = Math.min(y0, 0);
2961
+ y1 = Math.max(y1, 0);
2962
+ }
2963
+ return { x: [x0, x1], y: [y0, y1] };
2964
+ }
2965
+ function areaPaths(points, { width, height, pad = 4, xDomain, yDomain }) {
2966
+ const { line, area, zeroY } = linePaths(points, { width, height, pad, xDomain, yDomain, includeZero: false });
2967
+ return { line, area, zeroY };
2968
+ }
2969
+ const STRINGS$4 = Object.freeze({
2970
+ empty: "No data yet",
2971
+ target: "Target",
2972
+ synced: "synced ",
2973
+ up: "up",
2974
+ down: "down",
2975
+ unchanged: "no change",
2976
+ trend: "trend, from",
2977
+ to: "to"
2978
+ });
2979
+ const HEADLINE_CHARS$2 = Object.freeze({ S: 10 });
2980
+ const SPARK_BOX = Object.freeze({ width: 240, height: 80 });
2981
+ const PAD$2 = 4;
2982
+ const MINUS$1 = "−";
2983
+ const UP = "▲";
2984
+ const DOWN = "▼";
2985
+ const round$2 = (v) => Math.round(v * 10) / 10;
2986
+ const isObj$1 = (v) => v !== null && typeof v === "object";
2987
+ const str$1 = (v) => typeof v === "string" ? v : "";
2988
+ function seriesPoints(series, scale2) {
2989
+ if (!Array.isArray(series)) return { pts: [], dropped: 0 };
2990
+ const timed = [];
2991
+ const plain = [];
2992
+ let dropped = 0;
2993
+ for (const item of series) {
2994
+ if (isObj$1(item) && !(typeof item.toNumber === "function")) {
2995
+ const t = toTime(item.t);
2996
+ const v = toNumber(item.v) / scale2;
2997
+ if (Number.isFinite(t) && Number.isFinite(v)) timed.push({ x: t, y: v });
2998
+ else dropped++;
2999
+ } else {
3000
+ const v = toNumber(item) / scale2;
3001
+ if (Number.isFinite(v)) plain.push({ x: plain.length, y: v });
3002
+ else dropped++;
3003
+ }
3004
+ }
3005
+ if (timed.length) return { pts: timed.sort((a, b) => a.x - b.x), dropped: dropped + plain.length };
3006
+ return { pts: plain, dropped };
3007
+ }
3008
+ function metricModel(data, variant, ctx) {
3009
+ const fmt = ctx.fmt;
3010
+ const d = isObj$1(data) ? data : {};
3011
+ const format = ["money", "percent", "duration"].includes(d.format) ? d.format : "number";
3012
+ const decimals = Number.isInteger(d.decimals) && d.decimals >= 0 && d.decimals <= 20 ? d.decimals : void 0;
3013
+ const unit = str$1(d.unit);
3014
+ const compact = d.compact === true;
3015
+ const scale2 = format === "percent" && d.percentPoints === true ? 100 : 1;
3016
+ const plus = (s, n) => n > 0 && /[1-9]/.test(s) && s[0] !== "+" ? `+${s}` : s;
3017
+ const show = (n, sign2, compactForm) => {
3018
+ const o = { sign: sign2, decimals, compact: compactForm };
3019
+ switch (format) {
3020
+ case "money":
3021
+ return fmt.money(n, o);
3022
+ case "percent":
3023
+ return fmt.percent(n, o);
3024
+ case "duration": {
3025
+ const s = fmt.duration(Math.abs(n));
3026
+ return Number.isFinite(n) && n < 0 ? `${MINUS$1}${s}` : sign2 === "always" ? plus(s, n) : s;
3027
+ }
3028
+ default: {
3029
+ const s = unit ? fmt.unit(n, unit, o) : fmt.number(n, { ...o, sign: "negative" });
3030
+ return sign2 === "always" ? plus(s, n) : s;
3031
+ }
3032
+ }
3033
+ };
3034
+ const value = toNumber(d.value) / scale2;
3035
+ const known = Number.isFinite(value);
3036
+ let headline = "—";
3037
+ let spoken = "";
3038
+ if (known) {
3039
+ spoken = show(value, "negative", compact);
3040
+ headline = spoken;
3041
+ if (variant === "S" && !compact) {
3042
+ if (format === "money" || format === "percent" || format === "number" && !unit) {
3043
+ headline = fmt.fit(value, HEADLINE_CHARS$2.S, { style: format, decimals, sign: "negative" });
3044
+ spoken = show(value, "negative", false);
3045
+ } else if (headline.length > HEADLINE_CHARS$2.S) {
3046
+ const short = show(value, "negative", true);
3047
+ if (short.length < headline.length) headline = short;
3048
+ }
3049
+ }
3050
+ }
3051
+ const previous = toNumber(d.previous) / scale2;
3052
+ const givenAbs = toNumber(d.delta) / scale2;
3053
+ let abs = Number.isFinite(givenAbs) ? givenAbs : known && Number.isFinite(previous) ? value - previous : NaN;
3054
+ let pct = toNumber(d.deltaPct);
3055
+ if (!Number.isFinite(pct)) {
3056
+ pct = Number.isFinite(abs) && Number.isFinite(previous) && previous !== 0 ? abs / Math.abs(previous) : NaN;
3057
+ }
3058
+ let delta2 = "";
3059
+ let spokenDelta = "";
3060
+ if (Number.isFinite(pct)) {
3061
+ const dp = Math.abs(pct) >= 0.1 || pct === 0 ? 0 : 1;
3062
+ delta2 = fmt.percent(pct, { sign: "always", decimals: dp });
3063
+ spokenDelta = fmt.percent(Math.abs(pct), { sign: "negative", decimals: dp });
3064
+ } else if (Number.isFinite(abs)) {
3065
+ delta2 = show(abs, "always", compact);
3066
+ spokenDelta = show(Math.abs(abs), "negative", compact);
3067
+ }
3068
+ const rise = delta2.startsWith("+");
3069
+ const fall = delta2.startsWith(MINUS$1);
3070
+ const arrow = rise ? UP : fall ? DOWN : "";
3071
+ let tone2 = "mut";
3072
+ const goodWhen = d.goodWhen === "down" || d.goodWhen === "neither" ? d.goodWhen : "up";
3073
+ if (goodWhen !== "neither" && (rise || fall)) tone2 = rise === (goodWhen === "up") ? "pos" : "neg";
3074
+ const compare = str$1(d.compareLabel);
3075
+ const label = ctx.label;
3076
+ let ariaLabel = known ? `${label}, ${spoken}` : label;
3077
+ if (known && delta2) {
3078
+ const how = rise ? `${STRINGS$4.up} ${spokenDelta}` : fall ? `${STRINGS$4.down} ${spokenDelta}` : STRINGS$4.unchanged;
3079
+ ariaLabel += `, ${how}${compare ? ` ${compare}` : ""}`;
3080
+ }
3081
+ const vm = {
3082
+ ariaLabel,
3083
+ label,
3084
+ live: d.live === true,
3085
+ time: "",
3086
+ value: headline,
3087
+ delta: delta2,
3088
+ arrow,
3089
+ tone: tone2,
3090
+ compare,
3091
+ line: "",
3092
+ area: "",
3093
+ sparkLabel: "",
3094
+ targetPct: "",
3095
+ targetWidth: 0,
3096
+ caption: "",
3097
+ dropped: 0
3098
+ };
3099
+ const asOf = toTime(d.asOf);
3100
+ if (Number.isFinite(asOf)) vm.time = fmt.time(asOf);
3101
+ if (variant === "Wide" || variant === "M") {
3102
+ const { pts, dropped } = seriesPoints(d.series, scale2);
3103
+ vm.dropped = dropped;
3104
+ if (pts.length >= 2) {
3105
+ const { line, x, y } = linePaths(pts, { ...SPARK_BOX, pad: PAD$2, includeZero: false });
3106
+ const floor = SPARK_BOX.height - PAD$2;
3107
+ vm.line = line;
3108
+ vm.area = `${line} L${round$2(x(pts[pts.length - 1].x))} ${floor} L${round$2(x(pts[0].x))} ${floor} Z`;
3109
+ vm.sparkLabel = `${label} ${STRINGS$4.trend} ${show(pts[0].y, "negative", compact)} ${STRINGS$4.to} ${show(pts[pts.length - 1].y, "negative", compact)}`;
3110
+ }
3111
+ }
3112
+ if (variant === "M") {
3113
+ vm.caption = str$1(d.caption);
3114
+ const target = toNumber(d.target) / scale2;
3115
+ if (known && Number.isFinite(target) && target > 0) {
3116
+ const ratio = value / target;
3117
+ vm.targetPct = fmt.percent(ratio, { sign: "negative", decimals: 0 });
3118
+ vm.targetWidth = Math.round(Math.min(1, Math.max(0, ratio)) * 1e3) / 1e3;
3119
+ }
3120
+ }
3121
+ return vm;
3122
+ }
3123
+ const delta = `<span class="delta" data-k="deltabox" hidden><span class="n" data-k="change"><span aria-hidden="true" data-k="arrow"></span> <span class="nv" data-k="delta"></span></span><span class="mut ell" data-k="compare"></span></span>`;
3124
+ const dot$2 = '<span class="dot live" data-k="dot" hidden></span>';
3125
+ const head$2 = `<div class="hd">${dot$2}<span class="lb ell" data-k="label"></span><span class="f1"></span><span class="sm mut n meta" data-k="meta" hidden><span class="pre">${STRINGS$4.synced}</span><span data-k="time"></span></span></div>`;
3126
+ const spark = (cls) => `
3127
+ <div class="${cls}"><svg viewBox="0 0 ${SPARK_BOX.width} ${SPARK_BOX.height}" preserveAspectRatio="none" role="img" data-k="spark" hidden>
3128
+ <path class="sa" data-k="area"></path><path class="sl" data-k="line"></path>
3129
+ </svg></div>`;
3130
+ const TEMPLATES$2 = {
3131
+ S: `${head$2}
3132
+ <span class="big nv" data-k="value"></span>
3133
+ <span class="f1"></span>
3134
+ ${delta}`,
3135
+ Wide: `
3136
+ <div class="cols">
3137
+ <div class="col lhs">${head$2}<span class="xbig nv" data-k="value"></span>${delta}</div>
3138
+ ${spark("rhs")}
3139
+ </div>`,
3140
+ M: `${head$2}
3141
+ <div class="rowb wrap"><span class="xbig nv" data-k="value"></span>${delta}</div>
3142
+ ${spark("plot")}
3143
+ <div class="target" data-k="target" hidden>
3144
+ <div class="rowb"><span class="lb f1">${STRINGS$4.target}</span><span class="sm n" data-k="tpct"></span></div>
3145
+ <div class="tbar" aria-hidden="true"><svg viewBox="0 0 100 6" preserveAspectRatio="none"><rect class="fill" data-k="fill" x="0" y="0" width="0" height="6"></rect></svg></div>
3146
+ </div>
3147
+ <span class="sm mut ell" data-k="caption"></span>`
3148
+ };
3149
+ const SKELETONS$4 = {
3150
+ S: `<div class="hd"><span class="lb ell" data-k="label"></span></div>${sk(60, 30)}<span class="f1"></span>${sk(70, 11)}`,
3151
+ Wide: `<div class="cols">
3152
+ <div class="col lhs"><div class="hd"><span class="lb ell" data-k="label"></span></div>${sk(70, 44)}${sk(85, 11)}</div>
3153
+ <div class="rhs"><span class="sk grow"></span></div>
3154
+ </div>`,
3155
+ M: `<div class="hd"><span class="lb ell" data-k="label"></span></div>
3156
+ <div class="rowb wrap">${sk(35, 44)}${sk(40, 11)}</div>
3157
+ <div class="plot"><span class="sk grow"></span></div>
3158
+ <div class="target"><div class="rowb">${sk(20, 9)}<span class="f1"></span>${sk(30, 11)}</div>${sk(100, 6)}</div>
3159
+ ${sk(65, 11)}`
3160
+ };
3161
+ class TsMetric extends TsWidget {
3162
+ static template(variant) {
3163
+ return TEMPLATES$2[variant] || TEMPLATES$2.Wide;
3164
+ }
3165
+ static skeleton(variant) {
3166
+ return SKELETONS$4[variant] || SKELETONS$4.Wide;
3167
+ }
3168
+ static metricModel(data, variant, ctx) {
3169
+ return metricModel(data, variant, ctx);
3170
+ }
3171
+ static viewModel(data, variant, ctx) {
3172
+ return metricModel(data, variant, ctx);
3173
+ }
3174
+ update(vm, variant) {
3175
+ const text = (k2, s) => {
3176
+ const el = this.$(`[data-k="${k2}"]`);
3177
+ if (el && el.textContent !== s) el.textContent = s;
3178
+ return el;
3179
+ };
3180
+ const attr = (el, name, v) => {
3181
+ if (el && el.getAttribute(name) !== v) el.setAttribute(name, v);
3182
+ };
3183
+ this.$('[data-k="dot"]').toggleAttribute("hidden", !vm.live);
3184
+ text("label", vm.label);
3185
+ this.$('[data-k="meta"]').toggleAttribute("hidden", !vm.time);
3186
+ text("time", vm.time);
3187
+ text("value", vm.value);
3188
+ const box = this.$('[data-k="deltabox"]');
3189
+ box.toggleAttribute("hidden", !vm.delta);
3190
+ text("arrow", vm.arrow);
3191
+ attr(this.$('[data-k="change"]'), "class", `n ${vm.tone}`);
3192
+ text("delta", vm.delta);
3193
+ text("compare", vm.compare);
3194
+ if (variant === "S") return;
3195
+ const svg = this.$('[data-k="spark"]');
3196
+ svg.toggleAttribute("hidden", !vm.line);
3197
+ attr(svg, "class", vm.tone);
3198
+ attr(svg, "aria-label", vm.sparkLabel);
3199
+ attr(this.$('[data-k="line"]'), "d", vm.line);
3200
+ attr(this.$('[data-k="area"]'), "d", vm.area);
3201
+ if (variant !== "M") return;
3202
+ this.$('[data-k="target"]').toggleAttribute("hidden", !vm.targetPct);
3203
+ text("tpct", vm.targetPct);
3204
+ attr(this.$('[data-k="fill"]'), "width", String(Math.round(vm.targetWidth * 100 * 10) / 10));
3205
+ text("caption", vm.caption);
3206
+ }
3207
+ }
3208
+ __publicField(TsMetric, "catalogKey", "metric");
3209
+ __publicField(TsMetric, "emptyText", STRINGS$4.empty);
3210
+ // value is the one required key: a tile without it has nothing to say, and
3211
+ // naming it is what lets a wrong-shape object be explained (RFC 0002 §2.7).
3212
+ __publicField(TsMetric, "dataShape", {
3213
+ value: "numeric",
3214
+ format: { kind: "string", optional: true },
3215
+ decimals: { kind: "number", optional: true },
3216
+ unit: { kind: "string", optional: true },
3217
+ compact: { kind: "boolean", optional: true },
3218
+ percentPoints: { kind: "boolean", optional: true },
3219
+ previous: { kind: "numeric", optional: true },
3220
+ delta: { kind: "numeric", optional: true },
3221
+ deltaPct: { kind: "numeric", optional: true },
3222
+ compareLabel: { kind: "string", optional: true },
3223
+ goodWhen: { kind: "string", optional: true },
3224
+ series: { kind: "array", optional: true },
3225
+ target: { kind: "numeric", optional: true },
3226
+ caption: { kind: "string", optional: true },
3227
+ asOf: { kind: "time", optional: true },
3228
+ live: { kind: "boolean", optional: true }
3229
+ });
3230
+ __publicField(TsMetric, "styles", `
3231
+ [hidden] { display: none; }
3232
+ /* Figures keep their sign beside their digits in right-to-left pages, and a
3233
+ value too wide for its slot clips rather than running into its neighbour. */
3234
+ .nv { white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: clip; unicode-bidi: isolate; }
3235
+ .n { unicode-bidi: isolate; }
3236
+ /* On a tile narrower than 200px the meta says '14:05', so the label keeps its room. */
3237
+ .meta { flex: none; white-space: nowrap; }
3238
+ @container (max-width: 200px) { .pre { display: none; } }
3239
+ .delta { display: flex; align-items: baseline; gap: 6px; min-width: 0; white-space: nowrap; font-size: 12px; }
3240
+ .delta .n { font-weight: var(--ts-num-weight-small, 500); flex: none; }
3241
+ .wrap { flex-wrap: wrap; gap: 2px 20px; }
3242
+ .cols { flex: 1; display: flex; gap: 24px; min-height: 0; }
3243
+ .lhs { flex: 1.1; min-width: 0; justify-content: space-between; }
3244
+ .rhs { flex: 1; min-width: 0; position: relative; }
3245
+ @container (max-width: 420px) { .cols { gap: 12px; } .lhs { flex: 1.3; } }
3246
+ .plot { flex: 1; min-height: 0; position: relative; }
3247
+ .rhs svg, .plot svg { position: absolute; inset: 0; width: 100%; height: 100%; direction: ltr; }
3248
+ .sl { fill: none; stroke: currentColor; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }
3249
+ .sa { fill: currentColor; fill-opacity: .12; stroke: none; }
3250
+ svg.pos { color: var(--ts-profit); }
3251
+ svg.neg { color: var(--ts-loss); }
3252
+ svg.mut { color: var(--ts-muted); }
3253
+ .target { display: flex; flex-direction: column; gap: 6px; }
3254
+ .tbar { height: 6px; border-radius: 3px; background: var(--ts-surface-2); overflow: hidden; }
3255
+ .tbar svg { display: block; width: 100%; height: 100%; }
3256
+ .fill { fill: var(--ts-accent); }
3257
+ `);
3258
+ const STRINGS$3 = Object.freeze({
3259
+ empty: "No data yet",
3260
+ synced: "synced ",
3261
+ last: "last",
3262
+ noData: "no data",
3263
+ series: "Series"
3264
+ });
3265
+ const MAX_SERIES = 4;
3266
+ const BOX = Object.freeze({ width: 400, height: 100 });
3267
+ const PAD$1 = 3;
3268
+ const TICK_STEPS = 3;
3269
+ const DASH$2 = "—";
3270
+ const MINUS = "−";
3271
+ const DAY_MS = 864e5;
3272
+ const round$1 = (v) => Math.round(v * 10) / 10;
3273
+ const isObj = (v) => v !== null && typeof v === "object";
3274
+ const str = (v) => typeof v === "string" ? v : "";
3275
+ function yColumns(y) {
3276
+ const list = Array.isArray(y) ? y : y === void 0 || y === null ? [] : [y];
3277
+ const out = [];
3278
+ for (const item of list) {
3279
+ if (typeof item === "string" && item) out.push({ key: item, name: item, kind: "line" });
3280
+ else if (isObj(item) && typeof item.key === "string" && item.key) {
3281
+ out.push({ key: item.key, name: str(item.name) || item.key, kind: item.kind === "area" ? "area" : "line" });
3282
+ }
3283
+ }
3284
+ return out;
3285
+ }
3286
+ const byX = (a, b) => a.x - b.x;
3287
+ function readSeries(d, readX) {
3288
+ const out = [];
3289
+ let dropped = 0;
3290
+ if (Array.isArray(d.series)) {
3291
+ d.series.forEach((s, i) => {
3292
+ const item = isObj(s) ? s : {};
3293
+ const points = [];
3294
+ if (Array.isArray(item.points)) {
3295
+ for (const p of item.points) {
3296
+ const x = isObj(p) ? readX(p.x) : NaN;
3297
+ const y = isObj(p) ? toNumber(p.y) : NaN;
3298
+ if (Number.isFinite(x) && Number.isFinite(y)) points.push({ x, y });
3299
+ else dropped++;
3300
+ }
3301
+ }
3302
+ out.push({ name: str(item.name) || `${STRINGS$3.series} ${i + 1}`, kind: item.kind === "area" ? "area" : "line", points: points.sort(byX) });
3303
+ });
3304
+ } else if (Array.isArray(d.rows)) {
3305
+ const cols = yColumns(d.y);
3306
+ const xKey = str(d.x);
3307
+ const lists = cols.map(() => []);
3308
+ for (const row of d.rows) {
3309
+ const x = isObj(row) && xKey ? readX(row[xKey]) : NaN;
3310
+ if (!Number.isFinite(x)) {
3311
+ dropped++;
3312
+ continue;
3313
+ }
3314
+ cols.forEach((col, i) => {
3315
+ const y = toNumber(row[col.key]);
3316
+ if (Number.isFinite(y)) lists[i].push({ x, y });
3317
+ else dropped++;
3318
+ });
3319
+ }
3320
+ cols.forEach((col, i) => out.push({ name: col.name, kind: col.kind, points: lists[i].sort(byX) }));
3321
+ }
3322
+ return { all: out, dropped };
3323
+ }
3324
+ const dayFormats = /* @__PURE__ */ new Map();
3325
+ function dayFormat(fmt, withYear) {
3326
+ const key = `${fmt.locale}|${fmt.timeZone}|${withYear}`;
3327
+ let f = dayFormats.get(key);
3328
+ if (!f) {
3329
+ const opts = withYear ? { day: "numeric", month: "short", year: "numeric" } : { day: "numeric", month: "short" };
3330
+ if (fmt.timeZone) opts.timeZone = fmt.timeZone;
3331
+ dayFormats.set(key, f = new Intl.DateTimeFormat(fmt.locale, opts));
3332
+ }
3333
+ return f;
3334
+ }
3335
+ function timeLabeller(fmt, x0, x1) {
3336
+ const span = x1 - x0;
3337
+ if (span < DAY_MS) return (t) => fmt.time(t);
3338
+ const f = dayFormat(fmt, span >= 365 * DAY_MS);
3339
+ return (t) => f.format(t);
3340
+ }
3341
+ function niceDomain(lo, hi, steps = TICK_STEPS) {
3342
+ const span = hi - lo || Math.abs(hi) || 1;
3343
+ const raw = span / steps;
3344
+ const clean = (v) => Number(v.toPrecision(12));
3345
+ for (let e = Math.floor(Math.log10(raw)); e < Math.floor(Math.log10(raw)) + 6; e++) {
3346
+ for (const m of [1, 2, 2.5, 3, 4, 5]) {
3347
+ const step = clean(m * 10 ** e);
3348
+ if (step < raw) continue;
3349
+ const start = clean(Math.floor(lo / step + 1e-9) * step);
3350
+ if (start + steps * step >= hi - step * 1e-9) {
3351
+ const ticks = [];
3352
+ for (let i = 0; i <= steps; i++) ticks.push(clean(start + i * step));
3353
+ return { lo: ticks[0], hi: ticks[steps], step, ticks };
3354
+ }
3355
+ }
3356
+ }
3357
+ return { lo, hi, step: raw, ticks: [lo, lo + raw, lo + 2 * raw, hi] };
3358
+ }
3359
+ function places(n) {
3360
+ const s = String(Number(n.toPrecision(12)));
3361
+ if (s.includes("e")) return 4;
3362
+ const dot2 = s.indexOf(".");
3363
+ return dot2 < 0 ? 0 : Math.min(4, s.length - dot2 - 1);
3364
+ }
3365
+ function chartModel(data, variant, ctx) {
3366
+ const fmt = ctx.fmt;
3367
+ const d = isObj(data) && !Array.isArray(data) ? data : {};
3368
+ const format = ["money", "percent", "duration"].includes(d.format) ? d.format : "number";
3369
+ const decimals = Number.isInteger(d.decimals) && d.decimals >= 0 && d.decimals <= 20 ? d.decimals : void 0;
3370
+ const unit = str(d.unit);
3371
+ const timeAxis = d.xType !== "number";
3372
+ const { all, dropped } = readSeries(d, timeAxis ? toTime : toNumber);
3373
+ const series = [];
3374
+ for (let i = 0; i < all.length && i < MAX_SERIES; i++) series.push(all[i]);
3375
+ const show = (n) => {
3376
+ const o = { sign: "negative", decimals };
3377
+ switch (format) {
3378
+ case "money":
3379
+ return fmt.money(n, o);
3380
+ case "percent":
3381
+ return fmt.percent(n, o);
3382
+ case "duration": {
3383
+ const s = fmt.duration(Math.abs(n));
3384
+ return Number.isFinite(n) && n < 0 ? `${MINUS}${s}` : s;
3385
+ }
3386
+ default:
3387
+ return unit ? fmt.unit(n, unit, { decimals }) : fmt.number(n, o);
3388
+ }
3389
+ };
3390
+ const tick = (n, step2) => {
3391
+ const dp = (s) => s < 1 ? places(s) : void 0;
3392
+ switch (format) {
3393
+ case "money":
3394
+ return fmt.money(n, { sign: "negative", compact: true, decimals: dp(step2) });
3395
+ case "percent":
3396
+ return fmt.percent(n, { sign: "negative", decimals: Math.min(3, places(step2 * 100)) });
3397
+ case "duration": {
3398
+ const s = fmt.duration(Math.abs(n));
3399
+ return n < 0 ? `${MINUS}${s}` : s;
3400
+ }
3401
+ default:
3402
+ return unit ? fmt.unit(n, unit, { compact: true, decimals: dp(step2) }) : fmt.number(n, { sign: "negative", compact: true, decimals: dp(step2) });
3403
+ }
3404
+ };
3405
+ const hasArea = series.some((s) => s.kind === "area");
3406
+ const dom = seriesDomain(series.map((s) => s.points), { includeZero: hasArea });
3407
+ const [x0, x1] = dom.x;
3408
+ const hasPoints = series.some((s) => s.points.length > 0);
3409
+ const ticked = variant === "M" || variant === "L" || variant === "XL";
3410
+ let [y0, y1] = dom.y;
3411
+ let ticks = [];
3412
+ let step = 0;
3413
+ if (hasPoints && ticked) {
3414
+ const nice = niceDomain(y0, y1);
3415
+ y0 = nice.lo;
3416
+ y1 = nice.hi;
3417
+ step = nice.step;
3418
+ ticks = nice.ticks;
3419
+ } else if (hasPoints) {
3420
+ const span = y1 - y0 || 1;
3421
+ y1 += span * 0.12;
3422
+ if (!hasArea) y0 -= span * 0.12;
3423
+ }
3424
+ const x = scale(x0, x1, PAD$1, BOX.width - PAD$1);
3425
+ const y = scale(y0, y1, BOX.height - PAD$1, PAD$1);
3426
+ const box = { width: BOX.width, height: BOX.height, pad: PAD$1, xDomain: [x0, x1], yDomain: [y0, y1] };
3427
+ const hasZero = hasPoints && y0 <= 0 && y1 >= 0;
3428
+ const zeroY = round$1(Math.min(BOX.height - PAD$1, Math.max(PAD$1, y(0))));
3429
+ const outSeries = series.map((s) => {
3430
+ const pts = s.points;
3431
+ const n = pts.length;
3432
+ if (!n) return { name: s.name, kind: s.kind, line: "", area: "", dot: "", last: DASH$2, empty: true };
3433
+ const lastPt = pts[n - 1];
3434
+ const last = show(lastPt.y);
3435
+ if (n === 1 && x1 > x0) {
3436
+ const px2 = round$1(x(lastPt.x));
3437
+ return { name: s.name, kind: s.kind, line: "", area: "", dot: `M${px2} ${round$1(y(lastPt.y))}h0`, last, empty: false };
3438
+ }
3439
+ const { line, area } = areaPaths(pts, box);
3440
+ const dotX = n === 1 ? round$1(BOX.width - PAD$1) : round$1(x(lastPt.x));
3441
+ return { name: s.name, kind: s.kind, line, area: s.kind === "area" ? area : "", dot: `M${dotX} ${round$1(y(lastPt.y))}h0`, last, empty: false };
3442
+ });
3443
+ const axis = { start: "", middle: "", end: "" };
3444
+ if (hasPoints) {
3445
+ const label = timeAxis ? timeLabeller(fmt, x0, x1) : (v) => fmt.number(v, { sign: "negative" });
3446
+ axis.start = label(x0);
3447
+ axis.middle = x1 > x0 ? label(x0 + (x1 - x0) / 2) : "";
3448
+ axis.end = x1 > x0 ? label(x1) : "";
3449
+ }
3450
+ let grid = "";
3451
+ const labels = [];
3452
+ if (ticks.length) {
3453
+ for (let i = TICK_STEPS; i >= 0; i--) {
3454
+ labels.push(tick(ticks[i], step));
3455
+ if (ticks[i] === 0) continue;
3456
+ const gy = round$1(PAD$1 + (TICK_STEPS - i) * (BOX.height - 2 * PAD$1) / TICK_STEPS);
3457
+ grid += `${grid ? " " : ""}M0 ${gy}H${BOX.width}`;
3458
+ }
3459
+ }
3460
+ const summary = outSeries.length ? outSeries.map((s) => `${s.name}, ${s.empty ? STRINGS$3.noData : `${STRINGS$3.last} ${s.last}`}`).join("; ") : `${ctx.label}, ${STRINGS$3.noData}`;
3461
+ const vm = {
3462
+ ariaLabel: summary,
3463
+ label: ctx.label,
3464
+ live: d.live === true,
3465
+ time: "",
3466
+ series: outSeries,
3467
+ legend: outSeries.map((s, i) => ({ name: s.name, index: i + 1 })),
3468
+ axis,
3469
+ zeroY,
3470
+ zero: hasZero,
3471
+ zeroPath: hasZero ? `M0 ${zeroY}H${BOX.width}` : "",
3472
+ grid,
3473
+ ticks: labels,
3474
+ dropped,
3475
+ extra: Math.max(0, all.length - MAX_SERIES)
3476
+ };
3477
+ const asOf = toTime(d.asOf);
3478
+ if (Number.isFinite(asOf)) vm.time = fmt.time(asOf);
3479
+ return vm;
3480
+ }
3481
+ const SLOTS = [1, 2, 3, 4];
3482
+ const dot$1 = '<span class="dot live" data-k="dot" hidden></span>';
3483
+ const meta = `<span class="sm mut n meta" data-k="meta" hidden><span class="pre">${STRINGS$3.synced}</span><span data-k="time"></span></span>`;
3484
+ const head$1 = (wide) => `<div class="hd">${dot$1}<span class="lb ell" data-k="label"></span>${wide ? '<span class="sname sm" data-k="first" hidden><span class="sw c-1" data-k="sw1" aria-hidden="true"></span><span class="ell" data-k="n1"></span></span>' : ""}<span class="f1"></span>${meta}</div>`;
3485
+ const marks = (cls, prefix) => SLOTS.map((i) => `<path class="${cls} c-${i}" data-k="${prefix}${i}"></path>`).join("");
3486
+ const plot = (ticked) => `<div class="prow"><div class="plot"><svg viewBox="0 0 ${BOX.width} ${BOX.height}" preserveAspectRatio="none" role="img" data-k="svg">${ticked ? '<path class="gl" data-k="grid"></path>' : ""}<path class="zl" data-k="zero"></path>${marks("sa", "a")}${marks("sl", "l")}${marks("sd", "d")}</svg></div>${ticked ? `<div class="yax n" aria-hidden="true"><div class="yin">${[0, 1, 2, 3].map((i) => `<span data-k="y${i}"></span>`).join("")}</div></div>` : ""}</div>`;
3487
+ const xax = (gutter) => `<div class="xax n${gutter ? " gut" : ""}" aria-hidden="true"><span data-k="x0"></span><span data-k="x1"></span><span data-k="x2"></span></div>`;
3488
+ const chart = (ticked) => `<div class="chartcol">${plot(ticked)}${xax(ticked)}</div>`;
3489
+ const legendItem = (i) => `<span class="li" data-k="lg${i}" hidden><span class="sw c-${i}" data-k="sw${i}" aria-hidden="true"></span><span class="ell" data-k="n${i}"></span><span class="n sm" data-k="v${i}"></span></span>`;
3490
+ const railItem = (i) => `<div class="ri" data-k="lg${i}" hidden><span class="li"><span class="sw c-${i}" data-k="sw${i}" aria-hidden="true"></span><span class="ell" data-k="n${i}"></span></span><span class="n md" data-k="v${i}"></span></div>`;
3491
+ const TEMPLATES$1 = {
3492
+ Wide: `${head$1(true)}${chart(false)}`,
3493
+ M: `${head$1(false)}<div class="legend">${SLOTS.map(legendItem).join("")}</div>${chart(true)}`,
3494
+ L: `${head$1(false)}<div class="crow">${chart(true)}<div class="rail">${SLOTS.map(railItem).join("")}</div></div>`
3495
+ };
3496
+ TEMPLATES$1.XL = TEMPLATES$1.L;
3497
+ const skAxis = '<div class="xax"><span class="sk wx h9"></span><span></span><span class="sk wx h9"></span></div>';
3498
+ const skHead = '<div class="hd"><span class="lb ell" data-k="label"></span></div>';
3499
+ const skRail = SLOTS.map(() => `<div class="ri">${sk(60, 10)}${sk(40, 16)}</div>`).join("");
3500
+ const SKELETONS$3 = {
3501
+ Wide: `${skHead}<div class="chartcol"><span class="sk grow"></span>${skAxis}</div>`,
3502
+ M: `${skHead}<div class="legend">${sk(30, 10)}${sk(25, 10)}</div><div class="chartcol"><span class="sk grow"></span>${skAxis}</div>`,
3503
+ L: `${skHead}<div class="crow"><div class="chartcol"><span class="sk grow"></span>${skAxis}</div><div class="rail">${skRail}</div></div>`
3504
+ };
3505
+ SKELETONS$3.XL = SKELETONS$3.L;
3506
+ class TsChart extends TsWidget {
3507
+ static template(variant) {
3508
+ return TEMPLATES$1[variant] || TEMPLATES$1.M;
3509
+ }
3510
+ static skeleton(variant) {
3511
+ return SKELETONS$3[variant] || SKELETONS$3.M;
3512
+ }
3513
+ static chartModel(data, variant, ctx) {
3514
+ return chartModel(data, variant, ctx);
3515
+ }
3516
+ // The result is spoken once, as docs/ACCESSIBILITY.md asks of a chart: the
3517
+ // region is named by its label alone and the svg's role=img carries the
3518
+ // summary. Folding the summary into the region name too made a screen reader
3519
+ // read it twice.
3520
+ static viewModel(data, variant, ctx) {
3521
+ const vm = chartModel(data, variant, ctx);
3522
+ return { ...vm, chartLabel: vm.ariaLabel, ariaLabel: ctx.label };
3523
+ }
3524
+ update(vm, variant) {
3525
+ const text = (k2, s) => {
3526
+ const el = this.$(`[data-k="${k2}"]`);
3527
+ if (el && el.textContent !== s) el.textContent = s;
3528
+ };
3529
+ const attr = (k2, name, v) => {
3530
+ const el = this.$(`[data-k="${k2}"]`);
3531
+ if (el && el.getAttribute(name) !== v) el.setAttribute(name, v);
3532
+ };
3533
+ const hide = (k2, on) => {
3534
+ const el = this.$(`[data-k="${k2}"]`);
3535
+ if (el) el.toggleAttribute("hidden", on);
3536
+ };
3537
+ if (vm.extra > 0) warnOnce(`series-cap:${this._uid}`, `${describeElement(this)} draws at most ${MAX_SERIES} series; ${MAX_SERIES + vm.extra} were given.`);
3538
+ hide("dot", !vm.live);
3539
+ text("label", vm.label);
3540
+ hide("meta", !vm.time);
3541
+ text("time", vm.time);
3542
+ attr("svg", "aria-label", vm.chartLabel === void 0 ? vm.ariaLabel : vm.chartLabel);
3543
+ for (const i of SLOTS) {
3544
+ const s = vm.series[i - 1];
3545
+ attr(`a${i}`, "d", s ? s.area : "");
3546
+ attr(`l${i}`, "d", s ? s.line : "");
3547
+ attr(`d${i}`, "d", s ? s.dot : "");
3548
+ hide(`lg${i}`, !s);
3549
+ if (variant === "Wide" && i === 1) hide("first", !s);
3550
+ if (!s) continue;
3551
+ attr(`sw${i}`, "class", `sw c-${i}${s.kind === "area" ? " area" : ""}`);
3552
+ text(`n${i}`, s.name);
3553
+ text(`v${i}`, s.last);
3554
+ }
3555
+ attr("zero", "d", vm.zeroPath);
3556
+ attr("grid", "d", vm.grid);
3557
+ for (let i = 0; i < 4; i++) text(`y${i}`, vm.ticks[i] || "");
3558
+ text("x0", vm.axis.start);
3559
+ text("x1", vm.axis.middle);
3560
+ text("x2", vm.axis.end);
3561
+ }
3562
+ }
3563
+ __publicField(TsChart, "catalogKey", "chart");
3564
+ __publicField(TsChart, "emptyText", STRINGS$3.empty);
3565
+ // Both forms are optional here because either alone is enough; naming the
3566
+ // series items' points is what lets a wrong-shape one be explained.
3567
+ __publicField(TsChart, "dataShape", {
3568
+ series: { kind: "array", optional: true, of: { name: { kind: "string", optional: true }, points: "array" } },
3569
+ rows: { kind: "array", optional: true },
3570
+ x: { kind: "string", optional: true },
3571
+ xType: { kind: "string", optional: true },
3572
+ format: { kind: "string", optional: true },
3573
+ decimals: { kind: "number", optional: true },
3574
+ unit: { kind: "string", optional: true },
3575
+ goodWhen: { kind: "string", optional: true },
3576
+ asOf: { kind: "time", optional: true },
3577
+ live: { kind: "boolean", optional: true }
3578
+ });
3579
+ __publicField(TsChart, "styles", `
3580
+ [hidden] { display: none !important; }
3581
+ .n { unicode-bidi: isolate; }
3582
+ .meta { flex: none; white-space: nowrap; }
3583
+ @container (max-width: 200px) { .pre { display: none; } }
3584
+ .chartcol { flex: 1; min-height: 0; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
3585
+ .prow { flex: 1; min-height: 0; display: flex; gap: 8px; }
3586
+ .plot { position: relative; flex: 1; min-width: 0; min-height: 0; }
3587
+ .plot svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; direction: ltr; }
3588
+ .yax { position: relative; width: 34px; flex: none; font-size: 10px; color: var(--ts-muted); }
3589
+ .yin { position: absolute; inset-inline: 0; inset-block: 3%; display: flex; flex-direction: column; justify-content: space-between; }
3590
+ .yin span { height: 0; display: flex; align-items: center; justify-content: flex-end; line-height: 1; white-space: nowrap; }
3591
+ .xax { display: grid; grid-template-columns: 1fr auto 1fr; gap: 8px; font-size: 11px; color: var(--ts-muted); min-height: 14px; }
3592
+ .xax > :last-child { justify-self: end; }
3593
+ .xax.gut { padding-inline-end: 42px; }
3594
+ .c-1 { --c: var(--ts-series-1); }
3595
+ .c-2 { --c: var(--ts-series-2); }
3596
+ .c-3 { --c: var(--ts-series-3); }
3597
+ .c-4 { --c: var(--ts-series-4); }
3598
+ .sl { fill: none; stroke: var(--c); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }
3599
+ .sa { fill: color-mix(in srgb, var(--c) var(--ts-fill, 12%), transparent); stroke: none; }
3600
+ .sd { fill: none; stroke: var(--c); stroke-width: 7; stroke-linecap: round; vector-effect: non-scaling-stroke; }
3601
+ .gl { fill: none; stroke: var(--ts-line); stroke-dasharray: 2 4; vector-effect: non-scaling-stroke; }
3602
+ .zl { fill: none; stroke: var(--ts-axis); stroke-dasharray: 3 3; vector-effect: non-scaling-stroke; }
3603
+ .sw { width: 12px; height: 3px; border-radius: 2px; flex: none; background: var(--c); }
3604
+ .sw.area { width: 10px; height: 10px; border-radius: 2px; background: color-mix(in srgb, var(--c) 30%, transparent); box-shadow: inset 0 2px 0 var(--c); }
3605
+ .sname { display: inline-flex; align-items: center; gap: 6px; min-width: 0; color: var(--ts-muted); }
3606
+ .legend { display: flex; flex-wrap: wrap; gap: 4px 16px; font-size: 11px; }
3607
+ .li { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
3608
+ .legend .n { color: var(--ts-muted); }
3609
+ .crow { flex: 1; min-height: 0; display: flex; gap: 20px; }
3610
+ .rail { width: 190px; flex: none; display: flex; flex-direction: column; justify-content: center; gap: 14px; border-inline-start: 1px solid var(--ts-line); padding-inline-start: 20px; }
3611
+ .ri { display: flex; flex-direction: column; gap: 3px; min-width: 0; font-size: 11px; }
3612
+ @container (max-width: 460px) {
3613
+ .crow { flex-direction: column; gap: 10px; }
3614
+ .rail { width: auto; flex-direction: row; flex-wrap: wrap; justify-content: flex-start; gap: 8px 16px; border-inline-start: 0; padding-inline-start: 0; }
3615
+ }
3616
+ `);
3617
+ const STRINGS$2 = Object.freeze({ empty: "No trades today", shortLabel: "Today", realised: "Realised", unrealised: "Unrealised", fees: "Fees" });
3618
+ const HEADLINE_CHARS$1 = Object.freeze({ S: 10, Wide: 11, M: 14 });
3619
+ const CHART_BOX = Object.freeze({ width: 375, height: 100 });
3620
+ const PAD = 4;
3621
+ const DASH$1 = "—";
3622
+ const tone = (v) => {
3623
+ const n = toNumber(v);
3624
+ return n > 0 ? "pos" : n < 0 ? "neg" : "";
3625
+ };
3626
+ const count = (v, fmt) => {
3627
+ const n = toNumber(v);
3628
+ return Number.isFinite(n) && n >= 0 ? fmt.number(n, { decimals: 0 }) : null;
3629
+ };
3630
+ const joined = (parts2) => parts2.filter(Boolean).join(" · ");
3631
+ function readings(intraday) {
3632
+ if (!Array.isArray(intraday)) return { pts: [], dropped: 0 };
3633
+ const out = [];
3634
+ for (const r of intraday) {
3635
+ const t = toTime(r && r.t);
3636
+ const v = toNumber(r && r.v);
3637
+ if (Number.isFinite(t) && Number.isFinite(v)) out.push({ x: t, y: v });
3638
+ }
3639
+ return { pts: out.sort((a, b) => a.x - b.x), dropped: intraday.length - out.length };
3640
+ }
3641
+ function valueAt(pts, t) {
3642
+ for (let i = 1; i < pts.length; i++) {
3643
+ const a = pts[i - 1];
3644
+ const b = pts[i];
3645
+ if (t <= b.x) return b.x === a.x ? b.y : a.y + (t - a.x) / (b.x - a.x) * (b.y - a.y);
3646
+ }
3647
+ return pts[pts.length - 1].y;
3648
+ }
3649
+ function domainOf(window2, pts) {
3650
+ const from = toTime(window2 && window2.from);
3651
+ const to = toTime(window2 && window2.to);
3652
+ if (Number.isFinite(from) && Number.isFinite(to) && to >= from) return [from, to];
3653
+ return pts.length ? [pts[0].x, pts[pts.length - 1].x] : null;
3654
+ }
3655
+ function midTick([from, to], fmt) {
3656
+ const mid = (from + to) / 2;
3657
+ const minuteOfDay = (t) => {
3658
+ const m = /^(\d{1,2}):(\d{2})/.exec(fmt.time(t));
3659
+ return m ? Number(m[1]) * 60 + Number(m[2]) : NaN;
3660
+ };
3661
+ for (const step of [30, 15]) {
3662
+ const ms = step * 6e4;
3663
+ const m = minuteOfDay(mid);
3664
+ if (!Number.isFinite(m)) break;
3665
+ const below = mid - (m % step * 6e4 + (mid % 6e4 + 6e4) % 6e4);
3666
+ const best = [below, below + ms].filter((t) => t > from && t < to).sort((a, b) => Math.abs(a - mid) - Math.abs(b - mid))[0];
3667
+ if (best !== void 0) return best;
3668
+ }
3669
+ return mid;
3670
+ }
3671
+ function summaryOf(pts, fmt) {
3672
+ if (!pts.length) return "";
3673
+ const last = pts[pts.length - 1];
3674
+ let lo = pts[0];
3675
+ let hi = pts[0];
3676
+ for (const p of pts) {
3677
+ if (p.y < lo.y) lo = p;
3678
+ if (p.y > hi.y) hi = p;
3679
+ }
3680
+ const at = (p) => p.y === last.y ? "now" : `at ${fmt.time(p.x)}`;
3681
+ return joined([`low ${fmt.money(lo.y, { decimals: 0 })} ${at(lo)}`, `high ${hi.y === last.y ? "" : `${fmt.money(hi.y, { decimals: 0 })} `}${at(hi)}`]);
3682
+ }
3683
+ function chartOf(data, pts, fmt) {
3684
+ const domain = domainOf(data.window, pts);
3685
+ const mid = domain ? midTick(domain, fmt) : NaN;
3686
+ const ticks = domain ? [domain[0], mid, domain[1]].map((t) => fmt.time(t)) : ["", "", ""];
3687
+ const span = domain ? domain[1] - domain[0] : 0;
3688
+ const frac = span > 0 ? (mid - domain[0]) / span : 0.5;
3689
+ const midAt = Math.round(Math.min(86, Math.max(14, (PAD + frac * (CHART_BOX.width - 2 * PAD)) / CHART_BOX.width * 100)) * 10) / 10;
3690
+ if (pts.length < 2) {
3691
+ return { empty: true, message: "No intraday data yet", ariaLabel: "Intraday P&L: no data yet", ticks, midAt, line: "", area: "", zeroY: 0, lastX: 0, marks: [] };
3692
+ }
3693
+ const { line, area, zeroY, x, y } = linePaths(pts, { ...CHART_BOX, pad: PAD, xDomain: domain });
3694
+ const first = pts[0];
3695
+ const last = pts[pts.length - 1];
3696
+ let low = first;
3697
+ for (const p of pts) if (p.y < low.y) low = p;
3698
+ const marks2 = [];
3699
+ let dropped = 0;
3700
+ for (const m of Array.isArray(data.marks) ? data.marks : []) {
3701
+ const t = toTime(m && m.t);
3702
+ if (!Number.isFinite(t)) {
3703
+ dropped++;
3704
+ continue;
3705
+ }
3706
+ if (t < first.x || t > last.x) continue;
3707
+ const kind = m.kind === "exit" ? "exit" : "entry";
3708
+ const px2 = x(t);
3709
+ if (px2 < 0 || px2 > CHART_BOX.width) continue;
3710
+ const name = m.label ? String(m.label) : kind === "exit" ? "Exit" : "Entry";
3711
+ marks2.push({
3712
+ kind,
3713
+ left: Math.round(px2 / CHART_BOX.width * 1e3) / 10,
3714
+ top: Math.round(y(valueAt(pts, t)) / CHART_BOX.height * 1e3) / 10,
3715
+ title: `${name} · ${fmt.time(t)}`
3716
+ });
3717
+ }
3718
+ return {
3719
+ empty: false,
3720
+ message: "",
3721
+ ariaLabel: `Intraday P&L: low ${fmt.money(low.y, { decimals: 0 })}, now ${fmt.money(last.y)}`,
3722
+ ticks,
3723
+ midAt,
3724
+ line,
3725
+ area,
3726
+ zeroY,
3727
+ lastX: Math.round(x(last.x) * 10) / 10,
3728
+ marks: marks2,
3729
+ droppedMarks: dropped
3730
+ };
3731
+ }
3732
+ function todayPnlModel(data, variant, ctx) {
3733
+ const fmt = ctx.fmt;
3734
+ const d = data || {};
3735
+ const net = toNumber(d.net);
3736
+ const asOf = toTime(d.asOf);
3737
+ const time = Number.isFinite(asOf) ? fmt.time(asOf) : "";
3738
+ const closed = count(d.closedCount, fmt);
3739
+ const open = count(d.openCount, fmt);
3740
+ const counts = joined([closed !== null && `${closed} closed`, open !== null && `${open} open`]);
3741
+ const fees = toNumber(d.fees);
3742
+ const feesText = Number.isFinite(fees) ? fmt.money(-Math.abs(fees)) : DASH$1;
3743
+ const feesPlain = Number.isFinite(fees) ? fmt.money(Math.abs(fees), { sign: "negative" }) : null;
3744
+ const vm = {
3745
+ ariaLabel: Number.isFinite(net) ? `${ctx.label}, ${fmt.money(net)}` : ctx.label,
3746
+ live: d.live === true,
3747
+ label: variant === "S" ? "Today" : ctx.label,
3748
+ time,
3749
+ net: fmt.fit(d.net, HEADLINE_CHARS$1[variant] || HEADLINE_CHARS$1.Wide),
3750
+ netTone: tone(d.net),
3751
+ realized: fmt.money(d.realized),
3752
+ realizedTone: tone(d.realized),
3753
+ unrealized: fmt.money(d.unrealized),
3754
+ unrealizedTone: tone(d.unrealized),
3755
+ fees: feesText,
3756
+ // Rows the tile couldn't read. Only the M variant draws the series, so
3757
+ // only it can say; the others have nothing to drop.
3758
+ dropped: 0
3759
+ };
3760
+ if (variant === "S") {
3761
+ vm.footer = joined([counts, feesPlain && `fees ${feesPlain}`]);
3762
+ } else if (variant === "Wide") {
3763
+ vm.note = joined(["net of fees", time]);
3764
+ } else if (variant === "M") {
3765
+ const { pts, dropped } = readings(d.intraday);
3766
+ vm.counts = counts;
3767
+ vm.summary = summaryOf(pts, fmt);
3768
+ vm.chart = chartOf(d, pts, fmt);
3769
+ vm.dropped = dropped + (vm.chart.droppedMarks || 0);
3770
+ }
3771
+ return vm;
3772
+ }
3773
+ const dot = '<span class="dot live" data-k="dot" hidden></span>';
3774
+ const TEMPLATES = {
3775
+ S: `
3776
+ <div class="hd">${dot}<span class="lb ell" data-k="label"></span><span class="f1"></span><span class="sm mut n" data-k="time"></span></div>
3777
+ <span class="big nv" data-k="net"></span>
3778
+ <span class="f1"></span>
3779
+ <span class="sm mut ell" data-k="footer"></span>`,
3780
+ Wide: `
3781
+ <div class="cols">
3782
+ <div class="col lhs">
3783
+ <div class="hd">${dot}<span class="lb ell" data-k="label"></span></div>
3784
+ <span class="xbig nv" data-k="net"></span>
3785
+ <span class="sm mut" data-k="note"></span>
3786
+ </div>
3787
+ <div class="col rhs">
3788
+ <div class="rowb"><span class="lb f1">${STRINGS$2.realised}</span><span class="n" data-k="realized"></span></div>
3789
+ <div class="rowb"><span class="lb f1">${STRINGS$2.unrealised}</span><span class="n" data-k="unrealized"></span></div>
3790
+ <div class="rowb"><span class="lb f1">${STRINGS$2.fees}</span><span class="n mut" data-k="fees"></span></div>
3791
+ </div>
3792
+ </div>`,
3793
+ M: `
3794
+ <div class="hd">${dot}<span class="lb ell" data-k="label"></span><span class="f1"></span><span class="sm mut" data-k="counts"></span></div>
3795
+ <div class="rowb base"><span class="xbig nv" data-k="net"></span><span class="sm mut" data-k="summary"></span></div>
3796
+ <div class="chart" part="chart">
3797
+ <svg viewBox="0 0 ${CHART_BOX.width} ${CHART_BOX.height}" preserveAspectRatio="none" role="img" data-k="svg">
3798
+ <defs>
3799
+ <clipPath id="up"><rect x="0" y="0" width="${CHART_BOX.width}" data-k="clip-up"></rect></clipPath>
3800
+ <clipPath id="dn"><rect x="0" width="${CHART_BOX.width}" data-k="clip-dn"></rect></clipPath>
3801
+ </defs>
3802
+ <line class="lz" x1="0" x2="${CHART_BOX.width}" data-k="zero"></line>
3803
+ <line class="lz" y1="0" y2="${CHART_BOX.height}" data-k="now"></line>
3804
+ <path class="ap" clip-path="url(#up)" data-k="area-up"></path>
3805
+ <path class="an" clip-path="url(#dn)" data-k="area-dn"></path>
3806
+ <path class="lp" clip-path="url(#up)" data-k="line-up"></path>
3807
+ <path class="ln" clip-path="url(#dn)" data-k="line-dn"></path>
3808
+ </svg>
3809
+ <div class="marks" data-k="marks"></div>
3810
+ <div class="nodata sm" data-k="nodata" hidden></div>
3811
+ </div>
3812
+ <div class="axis sm mut n"><span data-k="t0"></span><span class="mid" data-k="t1"></span><span data-k="t2"></span></div>
3813
+ <div class="g3 sep trio">
3814
+ <div class="col"><span class="lb">${STRINGS$2.realised}</span><span class="n md" data-k="realized"></span></div>
3815
+ <div class="col"><span class="lb">${STRINGS$2.unrealised}</span><span class="n md" data-k="unrealized"></span></div>
3816
+ <div class="col"><span class="lb">${STRINGS$2.fees}</span><span class="n md mut" data-k="fees"></span></div>
3817
+ </div>`
3818
+ };
3819
+ const SKELETONS$2 = {
3820
+ S: `<div class="hd"><span class="lb ell">${STRINGS$2.shortLabel}</span></div>${sk(65, 30)}<span class="f1"></span>${sk(80, 11)}`,
3821
+ Wide: `<div class="cols">
3822
+ <div class="col lhs"><div class="hd"><span class="lb ell" data-k="label"></span></div>${sk(75, 44)}${sk(60, 11)}</div>
3823
+ <div class="col rhs">${`<div class="rowb">${sk(40, 9)}<span class="f1"></span>${sk(35, 13)}</div>`.repeat(3)}</div>
3824
+ </div>`,
3825
+ M: `<div class="hd"><span class="lb ell" data-k="label"></span></div>
3826
+ <div class="rowb base">${sk(40, 44)}${sk(30, 11)}</div>
3827
+ <span class="sk grow"></span>
3828
+ <div class="axis">${sk("x", 9)}${sk("x", 9)}${sk("x", 9)}</div>
3829
+ <div class="g3 sep trio">${`<div class="col">${sk(60, 9)}${sk(75, 16)}</div>`.repeat(3)}</div>`
3830
+ };
3831
+ class TsTodayPnl extends TsWidget {
3832
+ static template(variant) {
3833
+ return TEMPLATES[variant] || TEMPLATES.Wide;
3834
+ }
3835
+ static skeleton(variant) {
3836
+ return SKELETONS$2[variant] || SKELETONS$2.Wide;
3837
+ }
3838
+ static viewModel(data, variant, ctx) {
3839
+ return todayPnlModel(data, variant, ctx);
3840
+ }
3841
+ update(vm, variant) {
3842
+ const text = (k2, s) => {
3843
+ const el = this.$(`[data-k="${k2}"]`);
3844
+ if (el && el.textContent !== s) el.textContent = s;
3845
+ return el;
3846
+ };
3847
+ const toned = (k2, base, s, t) => {
3848
+ const el = text(k2, s);
3849
+ if (el) el.className = t ? `${base} ${t}` : base;
3850
+ };
3851
+ this.$('[data-k="dot"]').toggleAttribute("hidden", !vm.live);
3852
+ text("label", vm.label);
3853
+ if (variant === "S") {
3854
+ text("time", vm.time);
3855
+ toned("net", "big nv", vm.net, vm.netTone);
3856
+ text("footer", vm.footer);
3857
+ return;
3858
+ }
3859
+ toned("net", "xbig nv", vm.net, vm.netTone);
3860
+ toned("realized", variant === "M" ? "n md" : "n", vm.realized, vm.realizedTone);
3861
+ toned("unrealized", variant === "M" ? "n md" : "n", vm.unrealized, vm.unrealizedTone);
3862
+ text("fees", vm.fees);
3863
+ if (variant === "Wide") {
3864
+ text("note", vm.note);
3865
+ return;
3866
+ }
3867
+ text("counts", vm.counts);
3868
+ text("summary", vm.summary);
3869
+ this._updateChart(vm.chart);
3870
+ text("t0", vm.chart.ticks[0]);
3871
+ text("t1", vm.chart.ticks[1]).style.left = `${vm.chart.midAt}%`;
3872
+ text("t2", vm.chart.ticks[2]);
3873
+ }
3874
+ _updateChart(c) {
3875
+ const svg = this.$('[data-k="svg"]');
3876
+ svg.setAttribute("aria-label", c.ariaLabel);
3877
+ svg.toggleAttribute("hidden", c.empty);
3878
+ const nodata = this.$('[data-k="nodata"]');
3879
+ nodata.textContent = c.message;
3880
+ nodata.toggleAttribute("hidden", !c.empty);
3881
+ const set = (k2, attrs) => {
3882
+ const el = this.$(`[data-k="${k2}"]`);
3883
+ for (const name in attrs) el.setAttribute(name, attrs[name]);
3884
+ };
3885
+ set("clip-up", { height: c.zeroY });
3886
+ set("clip-dn", { y: c.zeroY, height: CHART_BOX.height - c.zeroY });
3887
+ set("zero", { y1: c.zeroY, y2: c.zeroY });
3888
+ set("now", { x1: c.lastX, x2: c.lastX });
3889
+ set("area-up", { d: c.area });
3890
+ set("area-dn", { d: c.area });
3891
+ set("line-up", { d: c.line });
3892
+ set("line-dn", { d: c.line });
3893
+ this.$('[data-k="now"]').toggleAttribute("hidden", c.empty);
3894
+ this.$('[data-k="zero"]').toggleAttribute("hidden", c.empty);
3895
+ const box = this.$('[data-k="marks"]');
3896
+ const key = JSON.stringify(c.marks);
3897
+ if (box._key === key) return;
3898
+ box._key = key;
3899
+ box.replaceChildren(
3900
+ ...c.marks.map((m) => {
3901
+ const el = document.createElement("span");
3902
+ el.className = `mk ${m.kind}`;
3903
+ el.style.left = `${m.left}%`;
3904
+ el.style.top = `${m.top}%`;
3905
+ el.title = m.title;
3906
+ el.setAttribute("role", "img");
3907
+ el.setAttribute("aria-label", m.title);
3908
+ return el;
3909
+ })
3910
+ );
3911
+ }
3912
+ }
3913
+ __publicField(TsTodayPnl, "catalogKey", "todayPnl");
3914
+ __publicField(TsTodayPnl, "emptyText", STRINGS$2.empty);
3915
+ // The headline figures are required: a tile without them has nothing to say,
3916
+ // and naming them is what lets a wrong-shape object be explained (RFC 0002 §2.7).
3917
+ __publicField(TsTodayPnl, "dataShape", {
3918
+ net: "numeric",
3919
+ realized: "numeric",
3920
+ unrealized: "numeric",
3921
+ fees: "numeric",
3922
+ closedCount: { kind: "number", optional: true },
3923
+ openCount: { kind: "number", optional: true },
3924
+ intraday: { kind: "array", optional: true, of: { t: "time", v: "numeric" } },
3925
+ marks: { kind: "array", optional: true, of: { t: "time", kind: "string" } },
3926
+ window: { kind: "object", optional: true, of: { from: "time", to: "time" } },
3927
+ asOf: { kind: "time", optional: true },
3928
+ live: { kind: "boolean", optional: true }
3929
+ });
3930
+ __publicField(TsTodayPnl, "styles", `
3931
+ [hidden] { display: none; }
3932
+ /* Clip rather than overflow: a headline too wide for its slot must never
3933
+ run into the column beside it. fit() and the tile's scaled type make
3934
+ that rare; this makes it harmless. */
3935
+ .nv { white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: clip; }
3936
+ .cols { flex: 1; display: flex; gap: 24px; align-items: stretch; min-height: 0; }
3937
+ .lhs { flex: 1.1; min-width: 0; justify-content: space-between; }
3938
+ .rhs { flex: 1; min-width: 0; justify-content: center; gap: 10px; }
3939
+ /* Narrow tile (a 2-column phone): the three figures stack label over value
3940
+ and shrink to their content, so the headline gets the rest of the width.
3941
+ The note under the headline stays; it sits below the number, not beside it. */
3942
+ @container (max-width: 420px) {
3943
+ .cols { gap: 12px; }
3944
+ .lhs { flex: 1 1 0; }
3945
+ .rhs { flex: 0 0 auto; gap: 6px; }
3946
+ .rhs .rowb { flex-direction: column; align-items: flex-end; gap: 0; }
3947
+ }
3948
+ .base { gap: 2px 20px; flex-wrap: wrap; }
3949
+ .chart { position: relative; flex: 1; min-height: 0; }
3950
+ .chart svg { position: absolute; inset: 0; width: 100%; height: 100%; }
3951
+ .marks { position: absolute; inset: 0; pointer-events: none; }
3952
+ .mk {
3953
+ position: absolute; box-sizing: content-box; width: 8px; height: 8px; margin: -4px 0 0 -4px;
3954
+ border-radius: 50%; border: 2px solid var(--ts-surface); pointer-events: auto;
3955
+ }
3956
+ .mk.entry { background: var(--ts-accent); }
3957
+ .mk.exit { background: var(--ts-ink); }
3958
+ .nodata { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: var(--ts-muted); }
3959
+ .nodata[hidden] { display: none; }
3960
+ .axis { position: relative; display: flex; justify-content: space-between; min-height: 1.4em; }
3961
+ .axis .mid { position: absolute; top: 0; transform: translateX(-50%); }
3962
+ .trio { padding-top: 10px; }
3963
+ `);
3964
+ const RANGES = ["1W", "1M", "3M", "YTD", "All"];
3965
+ const HEADLINE_CHARS = { Wide: 10, M: 12, XL: 12 };
3966
+ const DIMS = { Wide: [400, 100], M: [600, 160], XL: [900, 220] };
3967
+ const DASH = "—";
3968
+ const STRINGS$1 = Object.freeze({ empty: "No trading days yet" });
3969
+ function normalizeRange(range) {
3970
+ if (typeof range !== "string") return "All";
3971
+ const wanted = range.trim().toUpperCase();
3972
+ return RANGES.find((r) => r.toUpperCase() === wanted) || "All";
3973
+ }
3974
+ const parts = (iso) => [Number(iso.slice(0, 4)), Number(iso.slice(5, 7)), Number(iso.slice(8, 10))];
3975
+ const two = (n) => String(n).padStart(2, "0");
3976
+ function shiftDays(iso, n) {
3977
+ const [y, m, d] = parts(iso);
3978
+ return new Date(Date.UTC(y, m - 1, d) + n * 864e5).toISOString().slice(0, 10);
3979
+ }
3980
+ function shiftMonths(iso, n) {
3981
+ const [y, m, d] = parts(iso);
3982
+ const t = y * 12 + (m - 1) - n;
3983
+ const ny = Math.floor(t / 12);
3984
+ const nm = t % 12 + 1;
3985
+ const last = new Date(Date.UTC(ny, nm, 0)).getUTCDate();
3986
+ return `${String(ny).padStart(4, "0")}-${two(nm)}-${two(Math.min(d, last))}`;
3987
+ }
3988
+ function rangeStart(latest, range) {
3989
+ switch (normalizeRange(range)) {
3990
+ case "1W":
3991
+ return shiftDays(latest, -6);
3992
+ case "1M":
3993
+ return shiftMonths(latest, 1);
3994
+ case "3M":
3995
+ return shiftMonths(latest, 3);
3996
+ case "YTD":
3997
+ return `${latest.slice(0, 4)}-01-01`;
3998
+ default:
3999
+ return "";
4000
+ }
4001
+ }
4002
+ function usable(days, timeZone) {
4003
+ if (!Array.isArray(days)) return [];
4004
+ const out = [];
4005
+ for (const d of days) {
4006
+ if (!d || !Number.isFinite(toNumber(d.pnl))) continue;
4007
+ const date = toDay(d.date, timeZone);
4008
+ if (date !== null) out.push(date === d.date ? d : { ...d, date });
4009
+ }
4010
+ return out;
4011
+ }
4012
+ function filterRange(days, range, timeZone = "UTC") {
4013
+ const rows = usable(days, timeZone);
4014
+ const r = normalizeRange(range);
4015
+ if (r === "All" || !rows.length) return rows;
4016
+ let latest = rows[0].date;
4017
+ for (const d of rows) if (d.date > latest) latest = d.date;
4018
+ const from = rangeStart(latest, r);
4019
+ return rows.filter((d) => d.date >= from);
4020
+ }
4021
+ const monthFormats = /* @__PURE__ */ new Map();
4022
+ function monthName(fmt, iso) {
4023
+ const key = String(fmt.locale);
4024
+ let f = monthFormats.get(key);
4025
+ if (!f) monthFormats.set(key, f = new Intl.DateTimeFormat(fmt.locale, { month: "short", timeZone: "UTC" }));
4026
+ const [y, m, d] = parts(iso);
4027
+ return f.format(Date.UTC(y, m - 1, d));
4028
+ }
4029
+ const sign = (v) => v > 1e-9 ? 1 : v < -1e-9 ? -1 : 0;
4030
+ const colour = (v) => sign(v) > 0 ? "pos" : sign(v) < 0 ? "neg" : "";
4031
+ const plural = (n, word, shown = n) => `${shown} ${word}${n === 1 ? "" : "s"}`;
4032
+ function equityCurveModel(data, variant, ctx) {
4033
+ const { fmt } = ctx;
4034
+ const label = ctx.label || WIDGETS.equityCurve.label;
4035
+ const d = data && typeof data === "object" ? data : {};
4036
+ const range = normalizeRange(ctx.range);
4037
+ const v = DIMS[variant] ? variant : "M";
4038
+ const [width, height] = DIMS[v];
4039
+ const timeZone = fmt.timeZone || "UTC";
4040
+ const all = usable(d.days, timeZone);
4041
+ const rows = filterRange(all, range, timeZone);
4042
+ const wholeSeries = rows.length === all.length;
4043
+ const series = equitySeries(rows, { timeZone });
4044
+ const stats = equityStats(rows, { timeZone });
4045
+ const n = series.length;
4046
+ const empty = n === 0;
4047
+ const givenTrades = toNumber(d.trades);
4048
+ const dayTrades = rows.some((r) => typeof r.trades === "number" && Number.isFinite(r.trades));
4049
+ const trades = wholeSeries && Number.isFinite(givenTrades) ? givenTrades : dayTrades ? stats.trades : null;
4050
+ const givenFees = toNumber(d.fees);
4051
+ const fees = wholeSeries && Number.isFinite(givenFees) ? fmt.money(givenFees, { sign: "negative" }) : "";
4052
+ const tradesText = trades === null ? "" : plural(trades, "trade", fmt.number(trades, { decimals: 0 }));
4053
+ const net = fmt.money(stats.net);
4054
+ const peak = fmt.money(stats.peak);
4055
+ const maxDrawdown = fmt.money(-stats.maxDrawdown);
4056
+ let lowest = n ? Infinity : 0;
4057
+ for (const s of series) lowest = Math.min(lowest, s.equity);
4058
+ const dayWord = plural(n, "day");
4059
+ const tradingDays = plural(n, "trading day");
4060
+ const chartLabel = empty ? `${label}: no trading days in this range` : `${label}: peak ${peak}, lowest ${fmt.money(lowest)}, ends at ${net}`;
4061
+ const points = series.map((s, i) => ({ x: i, y: s.equity }));
4062
+ const geo = linePaths(points, { width, height });
4063
+ let peakLine = "";
4064
+ let band = "";
4065
+ if (v === "XL" && n > 1) {
4066
+ const peaks = series.map((s, i) => ({ x: i, y: s.peak }));
4067
+ peakLine = linePath(peaks, geo.x, geo.y);
4068
+ band = bandPath(points, peaks, geo.x, geo.y);
4069
+ }
4070
+ const names = empty ? [] : [series[0], series[Math.floor((n - 1) / 2)], series[n - 1]].map((s) => monthName(fmt, s.date));
4071
+ let axis;
4072
+ if (v === "XL") {
4073
+ const [a, b, c] = empty ? ["", "", ""] : names;
4074
+ const mid = b === a || b === c ? "" : b;
4075
+ axis = [a, mid, c === a && !mid ? "" : c];
4076
+ } else {
4077
+ axis = empty ? ["", ""] : [names[0], names[2] === names[0] ? "" : names[2]];
4078
+ }
4079
+ return {
4080
+ variant: v,
4081
+ range,
4082
+ // Rows the series couldn't use: no readable date or no finite P&L.
4083
+ dropped: Array.isArray(d.days) ? d.days.length - all.length : 0,
4084
+ ranges: RANGES.map((value) => ({ value, on: value === range })),
4085
+ live: d.live === true,
4086
+ empty,
4087
+ // The short word fits Wide's 170px column; a host's own label is used as given.
4088
+ label: v === "Wide" ? `${label === WIDGETS.equityCurve.label ? "Equity" : label} · ${dayWord}` : label,
4089
+ headline: fmt.fit(stats.net, HEADLINE_CHARS[v]),
4090
+ net,
4091
+ netClass: colour(stats.net),
4092
+ peak,
4093
+ peakClass: colour(stats.peak),
4094
+ peakNote: `peak ${peak}`,
4095
+ maxDrawdown,
4096
+ maxDrawdownClass: colour(-stats.maxDrawdown),
4097
+ tradingDays: n,
4098
+ sub: `over ${tradingDays}`,
4099
+ daysLine: tradesText ? `${n} · ${tradesText}` : String(n),
4100
+ netOfFees: fees !== "",
4101
+ third: fees ? { label: "Fees", value: fees } : { label: "Trades", value: trades === null ? DASH : fmt.number(trades, { decimals: 0 }) },
4102
+ axis,
4103
+ chart: { width, height, line: geo.line, area: geo.area, zeroY: geo.zeroY, peakLine, band },
4104
+ chartLabel,
4105
+ ariaLabel: empty ? `${label}, no trading days in this range` : `${label}, ${net} over ${tradingDays}`
4106
+ };
4107
+ }
4108
+ const k = (name) => `[data-k="${name}"]`;
4109
+ function chartMarkup(v) {
4110
+ const [w, h] = DIMS[v];
4111
+ const xl = v === "XL";
4112
+ return `<div class="plot">
4113
+ <svg data-k="svg" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none" role="img" aria-label="">
4114
+ <defs><clipPath id="cu"><rect data-k="clipUp" x="0" y="0" width="${w}" height="0"></rect></clipPath><clipPath id="cd"><rect data-k="clipDn" x="0" y="0" width="${w}" height="0"></rect></clipPath></defs>
4115
+ ${xl ? '<path class="dd" data-k="band" d=""></path>' : ""}
4116
+ <line class="lz" data-k="zero" x1="0" x2="${w}" y1="0" y2="0"></line>
4117
+ ${xl ? '<path class="lz" data-k="peakLine" fill="none" d=""></path>' : '<path class="ap" data-k="areaUp" d="" clip-path="url(#cu)"></path><path class="an" data-k="areaDn" d="" clip-path="url(#cd)"></path>'}
4118
+ <path class="lp" data-k="lineUp" d="" clip-path="url(#cu)"></path><path class="ln" data-k="lineDn" d="" clip-path="url(#cd)"></path>
4119
+ </svg>
4120
+ <div class="empty" data-k="empty" hidden>No trading days in this range</div>
4121
+ </div>`;
4122
+ }
4123
+ function rangeButtons() {
4124
+ const buttons = RANGES.map((r) => `<button type="button" class="btn" data-range="${r}" aria-pressed="false">${r}</button>`).join("");
4125
+ return `<div role="group" aria-label="Range" class="row rng">${buttons}</div>`;
4126
+ }
4127
+ const plotBlock = '<div class="plot"><span class="sk"></span></div>';
4128
+ const stat = (a = 60, b = 75) => `<div class="col">${sk(a, 9)}${sk(b, 16)}</div>`;
4129
+ const head = '<div class="hd"><span class="lb" data-k="label"></span></div>';
4130
+ const SKELETONS$1 = {
4131
+ Wide: `<div class="rowl"><div class="col side"><span class="lb" data-k="label"></span>${sk(70, 30)}${sk(80, 11)}</div>${plotBlock}</div>`,
4132
+ M: `${head}<div class="rowb">${sk(40, 30)}${sk(25, 11)}</div>${plotBlock}<div class="row">${sk("x", 9)}<span class="f1"></span>${sk("x", 9)}</div><div class="g3 sep stats">${stat()}${stat()}${stat()}</div>`,
4133
+ XL: `<div class="rowl xl"><div class="chartcol">${head}${plotBlock}<div class="row">${sk("x", 9)}<span class="f1"></span>${sk("x", 9)}<span class="f1"></span>${sk("x", 9)}</div></div><div class="col sep rail">${stat(50, 70).repeat(4)}</div></div>`
4134
+ };
4135
+ class TsEquityCurve extends TsWidget {
4136
+ static get observedAttributes() {
4137
+ return [...super.observedAttributes, "range"];
4138
+ }
4139
+ static template(variant) {
4140
+ if (variant === "Wide") {
4141
+ return `<div class="rowl">
4142
+ <div class="col side"><span class="lb" data-k="label"></span><span class="big" data-k="net"></span><span class="sm mut" data-k="peakNote"></span></div>
4143
+ ${chartMarkup("Wide")}
4144
+ </div>`;
4145
+ }
4146
+ if (variant === "XL") {
4147
+ return `<div class="rowl xl">
4148
+ <div class="chartcol">
4149
+ <div class="hd"><span class="lb" data-k="label"></span><span class="f1"></span>${rangeButtons()}</div>
4150
+ ${chartMarkup("XL")}
4151
+ <div class="row sm mut"><span data-k="ax0"></span><span class="f1"></span><span data-k="ax1"></span><span class="f1"></span><span data-k="ax2"></span></div>
4152
+ </div>
4153
+ <div class="col sep rail">
4154
+ <div class="col"><span class="lb">Net</span><span class="big" data-k="net"></span></div>
4155
+ <div class="col"><span class="lb">Peak</span><span class="n md" data-k="peak"></span></div>
4156
+ <div class="col"><span class="lb">Max drawdown</span><span class="n md" data-k="maxDrawdown"></span><span class="sm mut">shaded on the chart</span></div>
4157
+ <div class="col"><span class="lb">Trading days</span><span class="n md" data-k="daysLine"></span></div>
4158
+ </div>
4159
+ </div>`;
4160
+ }
4161
+ return `<div class="hd"><span class="lb" data-k="label"></span><span class="dot live" data-k="live" hidden></span><span class="f1"></span><span class="sm mut" data-k="fees" hidden>net of fees</span></div>
4162
+ <div class="rowb"><span class="big" data-k="net"></span><span class="sm mut" data-k="sub"></span></div>
4163
+ ${chartMarkup("M")}
4164
+ <div class="row sm mut"><span data-k="ax0"></span><span class="f1"></span><span data-k="ax1"></span></div>
4165
+ <div class="g3 sep stats">
4166
+ <div class="col"><span class="lb">Peak</span><span class="n md" data-k="peak"></span></div>
4167
+ <div class="col"><span class="lb">Max drawdown</span><span class="n md" data-k="maxDrawdown"></span></div>
4168
+ <div class="col"><span class="lb" data-k="thirdLabel"></span><span class="n md" data-k="thirdValue"></span></div>
4169
+ </div>`;
4170
+ }
4171
+ static skeleton(variant) {
4172
+ return SKELETONS$1[variant] || SKELETONS$1.M;
4173
+ }
4174
+ // The range is the one thing the pure model needs beyond the data and formatter.
4175
+ viewContext() {
4176
+ return { range: this.range };
4177
+ }
4178
+ static viewModel(data, variant, ctx) {
4179
+ return equityCurveModel(data, variant, ctx);
4180
+ }
4181
+ get range() {
4182
+ return normalizeRange(this.getAttribute("range"));
4183
+ }
4184
+ set range(value) {
4185
+ if (value === null || value === void 0 || value === "") this.removeAttribute("range");
4186
+ else this.setAttribute("range", value);
4187
+ this.requestRender();
4188
+ }
4189
+ constructor() {
4190
+ super();
4191
+ this._section.addEventListener("click", (e) => {
4192
+ const button = e.target && e.target.closest ? e.target.closest("button[data-range]") : null;
4193
+ if (!button) return;
4194
+ const value = button.getAttribute("data-range");
4195
+ if (value === this.range) return;
4196
+ const previous = this.range;
4197
+ this.range = value;
4198
+ if (!this.emit("range", { value })) this.range = previous;
4199
+ });
4200
+ }
4201
+ connectedCallback() {
4202
+ this.upgrade("range");
4203
+ super.connectedCallback();
4204
+ }
4205
+ update(vm, variant) {
4206
+ const text = (key, value) => {
4207
+ const el = this.$(k(key));
4208
+ if (el && el.textContent !== value) el.textContent = value;
4209
+ };
4210
+ const attr = (key, name, value) => {
4211
+ const el = this.$(k(key));
4212
+ if (el) el.setAttribute(name, value);
4213
+ };
4214
+ const cls = (key, base, extra) => attr(key, "class", extra ? `${base} ${extra}` : base);
4215
+ text("label", vm.label);
4216
+ text("net", vm.headline);
4217
+ cls("net", "big", vm.netClass);
4218
+ if (variant === "Wide") text("peakNote", vm.peakNote);
4219
+ if (variant !== "Wide") {
4220
+ text("peak", vm.peak);
4221
+ cls("peak", "n md", vm.peakClass);
4222
+ text("maxDrawdown", vm.maxDrawdown);
4223
+ cls("maxDrawdown", "n md", vm.maxDrawdownClass);
4224
+ }
4225
+ if (variant === "M") {
4226
+ text("sub", vm.sub);
4227
+ const net = this.$(k("fees"));
4228
+ if (net) net.toggleAttribute("hidden", !vm.netOfFees);
4229
+ text("thirdLabel", vm.third.label);
4230
+ text("thirdValue", vm.third.value);
4231
+ const live = this.$(k("live"));
4232
+ if (live) live.toggleAttribute("hidden", !vm.live);
4233
+ }
4234
+ if (variant === "XL") {
4235
+ text("daysLine", vm.daysLine);
4236
+ for (const r of vm.ranges) {
4237
+ const button = this.$(`button[data-range="${r.value}"]`);
4238
+ if (!button) continue;
4239
+ button.setAttribute("class", r.on ? "btn on" : "btn");
4240
+ button.setAttribute("aria-pressed", r.on ? "true" : "false");
4241
+ }
4242
+ }
4243
+ vm.axis.forEach((name, i) => text(`ax${i}`, name));
4244
+ const c = vm.chart;
4245
+ attr("svg", "aria-label", vm.chartLabel);
4246
+ this.$(k("svg")).toggleAttribute("hidden", vm.empty);
4247
+ this.$(k("empty")).toggleAttribute("hidden", !vm.empty);
4248
+ attr("zero", "y1", c.zeroY);
4249
+ attr("zero", "y2", c.zeroY);
4250
+ attr("clipUp", "height", c.zeroY);
4251
+ attr("clipDn", "y", c.zeroY);
4252
+ attr("clipDn", "height", c.height - c.zeroY);
4253
+ attr("lineUp", "d", c.line);
4254
+ attr("lineDn", "d", c.line);
4255
+ if (variant === "XL") {
4256
+ attr("band", "d", c.band);
4257
+ attr("peakLine", "d", c.peakLine);
4258
+ } else {
4259
+ attr("areaUp", "d", c.area);
4260
+ attr("areaDn", "d", c.area);
4261
+ }
4262
+ }
4263
+ }
4264
+ __publicField(TsEquityCurve, "catalogKey", "equityCurve");
4265
+ __publicField(TsEquityCurve, "emptyText", STRINGS$1.empty);
4266
+ __publicField(TsEquityCurve, "dataShape", {
4267
+ days: { kind: "array", of: { date: "time", pnl: "numeric" } },
4268
+ fees: { kind: "numeric", optional: true },
4269
+ trades: { kind: "number", optional: true },
4270
+ asOf: { kind: "time", optional: true },
4271
+ live: { kind: "boolean", optional: true }
4272
+ });
4273
+ __publicField(TsEquityCurve, "styles", `
4274
+ [hidden] { display: none !important; }
4275
+ .rowl { flex: 1; min-height: 0; display: flex; flex-direction: row; align-items: stretch; gap: 20px; }
4276
+ .rowl.xl { gap: 24px; }
4277
+ .plot { position: relative; flex: 1; min-height: 0; min-width: 0; }
4278
+ .plot svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
4279
+ .plot .empty { position: absolute; inset: 0; font-size: 12px; }
4280
+ .plot .sk { position: absolute; inset: 0; width: auto; }
4281
+ .dd { fill: color-mix(in srgb, var(--ts-loss) 22%, transparent); }
4282
+ .side { width: 170px; flex: none; justify-content: space-between; }
4283
+ .chartcol { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 10px; }
4284
+ .rng { gap: 4px; }
4285
+ .rail { width: 220px; flex: none; gap: 16px; }
4286
+ .rail.sep { border-top: 0; border-left: 1px solid var(--ts-line); padding-left: 24px; }
4287
+ .stats { padding-top: 10px; }
4288
+ `);
4289
+ const DAY = 864e5;
4290
+ const MINUTE = 6e4;
4291
+ const MAX_TICKS = 2e3;
4292
+ const THIN_WINDOW = 60 * MINUTE;
4293
+ const MIN_TICK_GAP = 6;
4294
+ const MAX_TIMEFRAMES = 2;
4295
+ const GLYPHS = ["│", "┃"];
4296
+ const envCache = /* @__PURE__ */ new Map();
4297
+ function zoneEnv(timeZone, base) {
4298
+ const key = `${base.locale}|${base.currency}|${timeZone}`;
4299
+ let env = envCache.get(key);
4300
+ if (!env) {
4301
+ if (envCache.size > 20) envCache.clear();
4302
+ const fmt = createFormatter({ locale: base.locale, currency: base.currency, timeZone });
4303
+ const nameFmt = new Intl.DateTimeFormat(fmt.locale, { timeZone: fmt.timeZone, timeZoneName: "short" });
4304
+ const dayFmt = new Intl.DateTimeFormat(fmt.locale, { timeZone: fmt.timeZone, weekday: "short" });
4305
+ env = {
4306
+ fmt,
4307
+ // The short name can change with daylight saving, so it is read per instant.
4308
+ name(t) {
4309
+ const part = nameFmt.formatToParts(t).find((p) => p.type === "timeZoneName");
4310
+ return part ? part.value : "";
4311
+ },
4312
+ weekday: (t) => dayFmt.format(t)
4313
+ };
4314
+ envCache.set(key, env);
4315
+ }
4316
+ return env;
4317
+ }
4318
+ const round = (v, places2) => {
4319
+ const k2 = 10 ** places2;
4320
+ return Math.round(v * k2) / k2;
4321
+ };
4322
+ const words = (env, ms) => env.fmt.duration(ms, { style: "words" });
4323
+ function validCalendar(c) {
4324
+ return !!c && ["state", "session", "hours", "candleClose"].every((m) => typeof c[m] === "function");
4325
+ }
4326
+ function lookup(calendar, id) {
4327
+ if (!validCalendar(calendar)) return { error: "No calendar" };
4328
+ try {
4329
+ return { spec: calendar.session(id) };
4330
+ } catch (err) {
4331
+ if (err instanceof RangeError) return { error: `Unknown session ${id}` };
4332
+ throw err;
4333
+ }
4334
+ }
4335
+ function laneState(calendar, id, spec, now) {
4336
+ const st = calendar.state(id, now);
4337
+ let win = null;
4338
+ if (st.open) win = { open: st.startedAt, close: st.closesAt };
4339
+ else if (st.opensAt !== null) win = calendar.hours(id, localDate(st.opensAt, spec.timeZone));
4340
+ return {
4341
+ open: st.open,
4342
+ win,
4343
+ opensAt: st.opensAt,
4344
+ closesAt: st.closesAt,
4345
+ progress: st.open ? round(st.progress * 100, 2) : 0,
4346
+ // More than a day away, a weekday says more than a duration does.
4347
+ far: !st.open && st.opensAt !== null && st.opensAt - now > DAY
4348
+ };
4349
+ }
4350
+ const tickCache = /* @__PURE__ */ new Map();
4351
+ function tickItems(win, tfs) {
4352
+ const key = `${win.open}|${win.close}|${tfs.join(",")}`;
4353
+ let ticks = tickCache.get(key);
4354
+ if (!ticks) {
4355
+ if (tickCache.size > 32) tickCache.clear();
4356
+ const items = [];
4357
+ const length = win.close - win.open;
4358
+ const steps = tfs.map((tf) => round(tf * MINUTE / length * 100, 3));
4359
+ tfs.forEach((tf, k2) => {
4360
+ const step = tf * MINUTE;
4361
+ if (length / step > MAX_TICKS) return;
4362
+ for (let off = step; off < length; off += step) items.push({ l: round(off / length * 100, 3), k: k2, at: win.open + off });
4363
+ });
4364
+ ticks = { key, items, steps };
4365
+ tickCache.set(key, ticks);
4366
+ }
4367
+ return ticks;
4368
+ }
4369
+ function visibleTicks(lane, now, railPx) {
4370
+ const ticks = lane.ticks;
4371
+ const thickOk = ticks.steps.length > 1 && ticks.steps[1] / 100 * railPx >= MIN_TICK_GAP;
4372
+ const items = [];
4373
+ let lo = -1;
4374
+ let hi = -1;
4375
+ ticks.items.forEach((t, i) => {
4376
+ if (t.k === 0) {
4377
+ if (lane.open && t.at > now && t.at <= now + THIN_WINDOW) {
4378
+ if (lo < 0) lo = i;
4379
+ hi = i;
4380
+ items.push(t);
4381
+ }
4382
+ } else if (thickOk) items.push(t);
4383
+ });
4384
+ return { key: `${ticks.key}|${lo}|${hi}|${thickOk ? 1 : 0}`, items };
4385
+ }
4386
+ const validTimeframe = (t) => typeof t === "number" && Number.isFinite(t) && t > 0;
4387
+ function sanitiseTimeframes(list) {
4388
+ if (!Array.isArray(list)) return [];
4389
+ return list.filter(validTimeframe).slice(0, MAX_TIMEFRAMES);
4390
+ }
4391
+ function timeframeText(tf) {
4392
+ return tf >= 60 && tf % 60 === 0 ? `${tf / 60}h` : `${tf}m`;
4393
+ }
4394
+ function opensText(env, st, withTime) {
4395
+ if (st.far) return withTime ? `opens ${env.weekday(st.opensAt)} ${env.fmt.time(st.opensAt)}` : `opens ${env.weekday(st.opensAt)}`;
4396
+ return `opens ${env.fmt.time(st.opensAt)}`;
4397
+ }
4398
+ function ariaFor(name, env, st) {
4399
+ if (st.open) return `${name} open, closes ${env.fmt.time(st.closesAt)}`;
4400
+ if (st.opensAt === null) return `${name} closed, no session scheduled`;
4401
+ return `${name} closed, ${opensText(env, st, true)}`;
4402
+ }
4403
+ function sessionClockModel(data, variant, ctx) {
4404
+ const d = data && typeof data === "object" ? data : {};
4405
+ const given = Array.isArray(d.sessions) ? d.sessions : [];
4406
+ const ids = given.filter((s) => typeof s === "string");
4407
+ const now = ctx.now;
4408
+ const first = ids.length ? lookup(d.calendar, ids[0]) : { error: "No sessions" };
4409
+ const env = zoneEnv(first.spec ? first.spec.timeZone : ctx.fmt.timeZone, ctx.fmt);
4410
+ const vm = variant === "Banner" ? banner(d, ids, first, env, now, ctx) : small(d, ids, first, env, now, ctx);
4411
+ const badTimeframes = variant === "Banner" && Array.isArray(d.timeframes) ? d.timeframes.filter((t) => !validTimeframe(t)).length : 0;
4412
+ vm.dropped = given.length - ids.length + badTimeframes;
4413
+ return vm;
4414
+ }
4415
+ function small(d, ids, first, env, now, ctx) {
4416
+ const zone = env.name(now);
4417
+ const time = env.fmt.time(now);
4418
+ if (first.error) {
4419
+ return {
4420
+ variant: "S",
4421
+ live: false,
4422
+ title: ctx.label || "Session clock",
4423
+ titleShort: ctx.label || "Session clock",
4424
+ session: ids.length ? ids[0] : "",
4425
+ time,
4426
+ zone,
4427
+ progress: 0,
4428
+ from: "",
4429
+ to: "",
4430
+ middle: first.error,
4431
+ middleShort: first.error,
4432
+ ariaLabel: `${ctx.label}: ${first.error}`
4433
+ };
4434
+ }
4435
+ const id = ids[0];
4436
+ const spec = first.spec;
4437
+ const st = laneState(d.calendar, id, spec, now);
4438
+ let middle;
4439
+ let middleShort;
4440
+ if (st.open) {
4441
+ middleShort = words(env, st.closesAt - now);
4442
+ middle = `${middleShort} to close`;
4443
+ } else if (st.opensAt === null) {
4444
+ middle = "no session scheduled";
4445
+ middleShort = "no session";
4446
+ } else if (st.far) {
4447
+ middle = opensText(env, st, true);
4448
+ middleShort = `${env.weekday(st.opensAt)} ${env.fmt.time(st.opensAt)}`;
4449
+ } else {
4450
+ middleShort = words(env, st.opensAt - now);
4451
+ middle = `opens in ${middleShort}`;
4452
+ }
4453
+ return {
4454
+ variant: "S",
4455
+ live: st.open,
4456
+ title: st.open ? "Market open" : "Market closed",
4457
+ titleShort: st.open ? "Open" : "Closed",
4458
+ session: spec.label,
4459
+ time,
4460
+ zone,
4461
+ progress: st.progress,
4462
+ from: st.win ? env.fmt.time(st.win.open) : "",
4463
+ to: st.win ? env.fmt.time(st.win.close) : "",
4464
+ middle,
4465
+ middleShort,
4466
+ ariaLabel: `${ctx.label}: ${ariaFor(spec.label, env, st)}`
4467
+ };
4468
+ }
4469
+ function banner(d, ids, first, env, now, ctx) {
4470
+ const tfs = sanitiseTimeframes(d.timeframes);
4471
+ const lanes = [];
4472
+ const spoken = [];
4473
+ let nextAt = null;
4474
+ ids.forEach((id, i) => {
4475
+ const found = i === 0 ? first : lookup(d.calendar, id);
4476
+ if (found.error) {
4477
+ lanes.push({ id, name: id, error: found.error, open: false, hours: "", progress: 0, right: "", ticks: { key: "", items: [], steps: [] }, next: null });
4478
+ spoken.push(found.error);
4479
+ return;
4480
+ }
4481
+ const spec = found.spec;
4482
+ const st = laneState(d.calendar, id, spec, now);
4483
+ let next = null;
4484
+ if (st.open && tfs.length) {
4485
+ const closes = tfs.map((tf) => d.calendar.candleClose(id, now, tf)).filter((t) => t !== null);
4486
+ if (closes.length) {
4487
+ let at = Infinity;
4488
+ for (const t of closes) at = Math.min(at, t);
4489
+ next = round((at - st.win.open) / (st.win.close - st.win.open) * 100, 2);
4490
+ if (nextAt === null || at < nextAt) nextAt = at;
4491
+ }
4492
+ }
4493
+ let right;
4494
+ if (st.open) right = `${words(env, st.closesAt - now)} left`;
4495
+ else right = st.opensAt === null ? "no session" : opensText(env, st, false);
4496
+ lanes.push({
4497
+ id,
4498
+ name: spec.label,
4499
+ error: null,
4500
+ open: st.open,
4501
+ hours: st.win ? `${env.fmt.time(st.win.open)}–${env.fmt.time(st.win.close)}` : "",
4502
+ progress: st.progress,
4503
+ right,
4504
+ ticks: st.win ? tickItems(st.win, tfs) : { key: "", items: [], steps: [] },
4505
+ next
4506
+ });
4507
+ spoken.push(ariaFor(spec.label, env, st));
4508
+ });
4509
+ const zone = env.name(now);
4510
+ return {
4511
+ variant: "Banner",
4512
+ now,
4513
+ live: lanes.some((l) => l.open),
4514
+ title: "Sessions",
4515
+ clock: `${env.fmt.time(now, { seconds: true })} ${zone}`.trim(),
4516
+ legend: tfs.map((tf, i) => ({ mark: GLYPHS[i], text: `${timeframeText(tf)} close${i === 0 ? ", next hour" : ""}` })),
4517
+ nextClose: nextAt === null ? "" : `next close in ${env.fmt.duration(nextAt - now)}`,
4518
+ lanes,
4519
+ ariaLabel: `${ctx.label}: ${spoken.length ? spoken.join("; ") : "No sessions"}`
4520
+ };
4521
+ }
4522
+ const setText = (el, value) => {
4523
+ if (el && el.textContent !== value) el.textContent = value;
4524
+ };
4525
+ const LANE_HTML = '<div class="lane"><div class="lcol"><span class="lname sm ell"></span><span class="lhours xs mut n ell"></span></div><div class="lt"><div class="rail"></div><div class="lf"></div><div class="tks"></div><div class="mk"></div><span class="msg xs mut ell" hidden></span></div><span class="lr n sm mut"></span></div>';
4526
+ const STRINGS = Object.freeze({ empty: "No sessions set up" });
4527
+ const LABEL_HEAD = '<div class="hd"><span class="lb title" data-k="label"></span>';
4528
+ const SKELETON_LANE = `<div class="lane"><div class="lcol">${sk(80, 11)}${sk(60, 9)}</div><div class="f1">${sk(100, 4)}</div><div class="lr">${sk(70, 10)}</div></div>`;
4529
+ const SKELETONS = {
4530
+ S: `${LABEL_HEAD}</div><div class="rowb">${sk(45, 30)}${sk(25, 11)}</div><span class="f1"></span>${sk(100, 6)}<div class="row">${sk(15, 9)}<span class="f1"></span>${sk(15, 9)}</div>`,
4531
+ Banner: `${LABEL_HEAD}<span class="f1"></span>${sk(20, 9)}</div><div class="lanes">${SKELETON_LANE.repeat(2)}</div>`
4532
+ };
4533
+ class TsSessionClock extends TsWidget {
4534
+ static template(variant) {
4535
+ if (variant === "Banner") {
4536
+ return '<div class="hd"><span class="dot live"></span><span class="lb title"></span><span class="n sm clock ell"></span><span class="f1"></span><span class="legend xs mut"><span class="lg0"></span><span class="lg1"></span></span><span class="nx xs"></span></div><div class="lanes"></div>';
4537
+ }
4538
+ return '<div class="hd"><span class="dot live"></span><span class="lb title"><span class="tf"></span><span class="ts"></span></span><span class="f1"></span><span class="xs mut sess ell"></span></div><div class="rowb"><span class="big time"></span><span class="sm mut zone"></span></div><span class="f1"></span><div class="trk"><div class="fill"></div></div><div class="row xs mut ft"><span class="n from"></span><span class="f1"></span><span class="mid ell"><span class="mf"></span><span class="ms"></span></span><span class="f1"></span><span class="n to"></span></div>';
4539
+ }
4540
+ static skeleton(variant) {
4541
+ return SKELETONS[variant] || SKELETONS.S;
4542
+ }
4543
+ static viewModel(data, variant, ctx) {
4544
+ return sessionClockModel(data, variant, ctx);
4545
+ }
4546
+ connectedCallback() {
4547
+ super.connectedCallback();
4548
+ this.every(1e3);
4549
+ }
4550
+ update(vm, variant) {
4551
+ if (variant === "Banner") this._updateBanner(vm);
4552
+ else this._updateSmall(vm);
4553
+ }
4554
+ _updateSmall(vm) {
4555
+ this.$(".dot").hidden = !vm.live;
4556
+ setText(this.$(".tf"), vm.title);
4557
+ setText(this.$(".ts"), vm.titleShort);
4558
+ setText(this.$(".sess"), vm.session);
4559
+ setText(this.$(".time"), vm.time);
4560
+ setText(this.$(".zone"), vm.zone);
4561
+ this.$(".fill").style.width = `${vm.progress}%`;
4562
+ setText(this.$(".from"), vm.from);
4563
+ setText(this.$(".mf"), vm.middle);
4564
+ setText(this.$(".ms"), vm.middleShort);
4565
+ const mid = this.$(".mid");
4566
+ if (mid.title !== vm.middle) mid.title = vm.middle;
4567
+ setText(this.$(".to"), vm.to);
4568
+ }
4569
+ _updateBanner(vm) {
4570
+ this.$(".dot").hidden = !vm.live;
4571
+ setText(this.$(".title"), vm.title);
4572
+ setText(this.$(".clock"), vm.clock);
4573
+ this.$(".legend").hidden = vm.legend.length === 0;
4574
+ for (let i = 0; i < 2; i++) {
4575
+ const item = vm.legend[i];
4576
+ const el = this.$(`.lg${i}`);
4577
+ el.hidden = !item;
4578
+ setText(el, item ? `${item.mark} ${item.text}` : "");
4579
+ }
4580
+ const nx = this.$(".nx");
4581
+ nx.hidden = !vm.nextClose;
4582
+ setText(nx, vm.nextClose ? `▮ ${vm.nextClose}` : "");
4583
+ const rows = this._laneRows(vm.lanes);
4584
+ const widths = rows.map((row) => row.track.clientWidth);
4585
+ vm.lanes.forEach((lane, i) => {
4586
+ const row = rows[i];
4587
+ setText(row.name, lane.name);
4588
+ setText(row.hours, lane.error ? "" : lane.hours);
4589
+ setText(row.right, lane.right);
4590
+ row.msg.hidden = !lane.error;
4591
+ setText(row.msg, lane.error || "");
4592
+ row.rail.hidden = !!lane.error;
4593
+ row.fill.hidden = !!lane.error;
4594
+ row.fill.style.width = `${lane.progress}%`;
4595
+ const ticks = visibleTicks(lane, vm.now, widths[i]);
4596
+ if (row.tickKey !== ticks.key) {
4597
+ row.ticks.innerHTML = ticks.items.map((t) => `<i class="tk${t.k ? " b" : ""}" style="left:${t.l}%"></i>`).join("");
4598
+ row.tickKey = ticks.key;
4599
+ }
4600
+ row.mark.hidden = lane.next === null;
4601
+ if (lane.next !== null) row.mark.style.left = `${lane.next}%`;
4602
+ });
4603
+ }
4604
+ /** The lane rows, rebuilt only when the list of sessions or the container they live in changes. */
4605
+ _laneRows(lanes) {
4606
+ const host = this.$(".lanes");
4607
+ const key = lanes.map((l) => l.id).join("\n");
4608
+ const cur = this._lanes;
4609
+ if (cur && cur.host === host && cur.key === key) return cur.rows;
4610
+ host.innerHTML = lanes.map(() => LANE_HTML).join("");
4611
+ const rows = Array.from(host.querySelectorAll(".lane")).map((el) => ({
4612
+ name: el.querySelector(".lname"),
4613
+ hours: el.querySelector(".lhours"),
4614
+ right: el.querySelector(".lr"),
4615
+ rail: el.querySelector(".rail"),
4616
+ fill: el.querySelector(".lf"),
4617
+ track: el.querySelector(".lt"),
4618
+ ticks: el.querySelector(".tks"),
4619
+ mark: el.querySelector(".mk"),
4620
+ msg: el.querySelector(".msg"),
4621
+ tickKey: null
4622
+ }));
4623
+ this._lanes = { host, key, rows };
4624
+ return rows;
4625
+ }
4626
+ }
4627
+ __publicField(TsSessionClock, "catalogKey", "sessionClock");
4628
+ __publicField(TsSessionClock, "emptyText", STRINGS.empty);
4629
+ __publicField(TsSessionClock, "dataShape", {
4630
+ calendar: "object",
4631
+ sessions: { kind: "array", of: "string" },
4632
+ timeframes: { kind: "array", optional: true, of: "number" }
4633
+ });
4634
+ __publicField(TsSessionClock, "styles", `
4635
+ [hidden] { display: none !important; }
4636
+ .trk { position: relative; height: 6px; border-radius: 99px; background: var(--ts-surface-2); overflow: hidden; flex: none; }
4637
+ .fill, .lf { position: absolute; left: 0; width: 0; background: var(--ts-accent); border-radius: 99px; }
4638
+ .fill { top: 0; bottom: 0; }
4639
+ .ft { gap: 6px; }
4640
+ .ft .n { flex: none; }
4641
+ .mid { text-align: center; }
4642
+ .title { white-space: nowrap; flex: none; }
4643
+ .ts, .ms { display: none; }
4644
+ .legend { display: flex; gap: 10px; white-space: nowrap; }
4645
+ .nx { font-weight: 600; white-space: nowrap; }
4646
+ .lanes { display: flex; flex-direction: column; gap: 6px; flex: 1; min-height: 0; justify-content: center; }
4647
+ .lane { display: flex; align-items: center; gap: 14px; }
4648
+ .lcol { width: 110px; flex: none; display: flex; flex-direction: column; gap: 1px; }
4649
+ .lname { font-weight: 600; }
4650
+ .lt { position: relative; flex: 1; min-width: 0; height: 26px; }
4651
+ .rail, .lf { position: absolute; top: 11px; height: 4px; border-radius: 99px; }
4652
+ .rail { left: 0; right: 0; background: var(--ts-surface-2); }
4653
+ .tks { position: absolute; inset: 0; pointer-events: none; }
4654
+ .tk { position: absolute; top: 8px; width: 1px; height: 10px; background: var(--ts-muted); }
4655
+ .tk.b { top: 5px; height: 16px; background: var(--ts-ink); }
4656
+ .mk { position: absolute; top: 2px; left: 0; width: 6px; height: 22px; margin-left: -3px; border-radius: 2px; box-shadow: inset 0 0 0 2px var(--ts-ink); }
4657
+ .msg { position: absolute; left: 0; right: 0; top: 0; bottom: 0; display: flex; align-items: center; }
4658
+ .lr { width: 90px; flex: none; text-align: right; white-space: nowrap; }
4659
+ @container (max-width: 480px) { .legend { display: none; } }
4660
+ @container (max-width: 400px) { .lane { gap: 10px; } .lcol { width: 84px; } .lr { width: 72px; } }
4661
+ /* A phone-width S (~170px): short words so nothing wraps or truncates. */
4662
+ @container (max-width: 220px) { .tf, .mf { display: none; } .ts, .ms { display: inline; } }
4663
+ `);
4664
+ const isDevice = (d) => d === "desktop" || d === "phone";
4665
+ const deviceOfColumns = (columns) => columns > 0 ? columns === 2 ? "phone" : "desktop" : null;
4666
+ function normaliseDefaults(defaults) {
4667
+ var _a, _b, _c;
4668
+ if (defaults == null) return { desktop: null, phone: null };
4669
+ const twoDevices = typeof defaults === "object" && !Array.isArray(defaults) && !("profiles" in defaults) && "desktop" in defaults;
4670
+ if (!twoDevices) return { desktop: defaults, phone: defaults };
4671
+ return { desktop: (_a = defaults.desktop) != null ? _a : null, phone: (_c = (_b = defaults.phone) != null ? _b : defaults.desktop) != null ? _c : null };
4672
+ }
4673
+ function defaultStorage() {
4674
+ try {
4675
+ return globalThis.localStorage || null;
4676
+ } catch (e) {
4677
+ return null;
4678
+ }
4679
+ }
4680
+ function persistLayout(grid, options = {}) {
4681
+ const { key, defaults } = options;
4682
+ if (typeof key !== "string" || !key) throw new TypeError("persistLayout needs a non-empty `key`");
4683
+ const storage = "storage" in options ? options.storage : defaultStorage();
4684
+ const fallback = normaliseDefaults(defaults);
4685
+ let device = null;
4686
+ let disposed = false;
4687
+ const keyFor = (d) => `${key}:${d}`;
4688
+ function applyDefaults(d) {
4689
+ if (fallback[d] == null) return;
4690
+ grid.layout = readLayout(fallback[d], { unknown: "keep", device: d }).layout;
4691
+ }
4692
+ function load(d) {
4693
+ let raw = null;
4694
+ if (storage) {
4695
+ try {
4696
+ const text = storage.getItem(keyFor(d));
4697
+ if (text != null) raw = JSON.parse(text);
4698
+ } catch (e) {
4699
+ raw = null;
4700
+ }
4701
+ }
4702
+ if (raw == null) return applyDefaults(d);
4703
+ grid.layout = readLayout(raw, { unknown: "keep", device: d }).layout;
4704
+ }
4705
+ function switchTo(d) {
4706
+ if (!isDevice(d) || d === device) return;
4707
+ device = d;
4708
+ load(d);
4709
+ }
4710
+ function onColumns(event) {
4711
+ if (disposed || event.target !== grid) return;
4712
+ const detail = event.detail || {};
4713
+ switchTo(isDevice(detail.device) ? detail.device : deviceOfColumns(detail.columns));
4714
+ }
4715
+ function onChange(event) {
4716
+ if (disposed || event.target !== grid || !storage || !device) return;
4717
+ const layout = event.detail && event.detail.layout;
4718
+ if (!layout) return;
4719
+ try {
4720
+ storage.setItem(keyFor(device), JSON.stringify(layout));
4721
+ } catch (e) {
4722
+ }
4723
+ }
4724
+ grid.addEventListener("ts-columns-change", onColumns);
4725
+ grid.addEventListener("ts-layout-change", onChange);
4726
+ const measured = deviceOfColumns(grid.columns);
4727
+ if (measured) switchTo(measured);
4728
+ return {
4729
+ reset(which) {
4730
+ if (disposed) return;
4731
+ const d = isDevice(which) ? which : device;
4732
+ if (!d) return;
4733
+ if (storage) {
4734
+ try {
4735
+ storage.removeItem(keyFor(d));
4736
+ } catch (e) {
4737
+ }
4738
+ }
4739
+ if (d === device) applyDefaults(d);
4740
+ },
4741
+ dispose() {
4742
+ disposed = true;
4743
+ grid.removeEventListener("ts-columns-change", onColumns);
4744
+ grid.removeEventListener("ts-layout-change", onChange);
4745
+ }
4746
+ };
4747
+ }
4748
+ function define(tag, ctor) {
4749
+ if (typeof customElements === "undefined") return;
4750
+ ctor.tickslateVersion = version;
4751
+ const owner = customElements.get(tag);
4752
+ if (!owner) {
4753
+ customElements.define(tag, ctor);
4754
+ return;
4755
+ }
4756
+ if (owner === ctor) return;
4757
+ {
4758
+ const by = owner.tickslateVersion ? `Tickslate ${owner.tickslateVersion}` : "a non-Tickslate element";
4759
+ warnOnce(`define:${tag}`, `<${tag}> is already defined by ${by}, so this copy (${version}) was not registered.`);
4760
+ }
4761
+ }
4762
+ define("ts-grid", TsGrid);
4763
+ define("ts-card", TsCard);
4764
+ define("ts-metric", TsMetric);
4765
+ define("ts-chart", TsChart);
4766
+ define("ts-today-pnl", TsTodayPnl);
4767
+ define("ts-equity-curve", TsEquityCurve);
4768
+ define("ts-session-clock", TsSessionClock);
4769
+ export {
4770
+ TsCard,
4771
+ TsChart,
4772
+ TsEquityCurve,
4773
+ TsGrid,
4774
+ TsMetric,
4775
+ TsSessionClock,
4776
+ TsTodayPnl,
4777
+ TsWidget,
4778
+ persistLayout,
4779
+ version2 as version
4780
+ };
4781
+ //# sourceMappingURL=elements.dev.js.map