@fcannizzaro/streamdeck-react 0.1.9 → 0.1.11

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.
Files changed (74) hide show
  1. package/LICENSE +190 -21
  2. package/README.md +3 -1
  3. package/dist/action.d.ts +2 -2
  4. package/dist/action.js +2 -2
  5. package/dist/bundler-shared.d.ts +11 -0
  6. package/dist/bundler-shared.js +11 -0
  7. package/dist/context/event-bus.d.ts +1 -1
  8. package/dist/context/event-bus.js +1 -1
  9. package/dist/context/touchstrip-context.d.ts +2 -0
  10. package/dist/context/touchstrip-context.js +5 -0
  11. package/dist/devtools/bridge.d.ts +35 -7
  12. package/dist/devtools/bridge.js +153 -46
  13. package/dist/devtools/highlight.d.ts +6 -0
  14. package/dist/devtools/highlight.js +106 -57
  15. package/dist/devtools/index.js +6 -0
  16. package/dist/devtools/observers/lifecycle.d.ts +4 -4
  17. package/dist/devtools/server.d.ts +6 -1
  18. package/dist/devtools/server.js +6 -1
  19. package/dist/devtools/types.d.ts +50 -6
  20. package/dist/font-inline.d.ts +5 -1
  21. package/dist/font-inline.js +8 -3
  22. package/dist/hooks/animation.d.ts +154 -0
  23. package/dist/hooks/animation.js +381 -0
  24. package/dist/hooks/events.js +1 -5
  25. package/dist/hooks/touchstrip.d.ts +6 -0
  26. package/dist/hooks/touchstrip.js +37 -0
  27. package/dist/index.d.ts +7 -2
  28. package/dist/index.js +3 -2
  29. package/dist/manifest-codegen.d.ts +38 -0
  30. package/dist/manifest-codegen.js +110 -0
  31. package/dist/node_modules/.bun/xxhash-wasm@1.1.0/node_modules/xxhash-wasm/esm/xxhash-wasm.js +3157 -0
  32. package/dist/plugin.js +20 -9
  33. package/dist/reconciler/host-config.js +19 -1
  34. package/dist/reconciler/vnode.d.ts +26 -0
  35. package/dist/reconciler/vnode.js +41 -10
  36. package/dist/render/buffer-pool.d.ts +19 -0
  37. package/dist/render/buffer-pool.js +51 -0
  38. package/dist/render/cache.d.ts +41 -0
  39. package/dist/render/cache.js +159 -5
  40. package/dist/render/image-cache.d.ts +53 -0
  41. package/dist/render/image-cache.js +128 -0
  42. package/dist/render/metrics.d.ts +58 -0
  43. package/dist/render/metrics.js +101 -0
  44. package/dist/render/pipeline.d.ts +46 -1
  45. package/dist/render/pipeline.js +370 -36
  46. package/dist/render/png.d.ts +10 -1
  47. package/dist/render/png.js +31 -13
  48. package/dist/render/render-pool.d.ts +26 -0
  49. package/dist/render/render-pool.js +141 -0
  50. package/dist/render/svg.d.ts +7 -0
  51. package/dist/render/svg.js +139 -0
  52. package/dist/render/worker.d.ts +1 -0
  53. package/dist/rollup.d.ts +23 -9
  54. package/dist/rollup.js +24 -9
  55. package/dist/roots/flush-coordinator.d.ts +18 -0
  56. package/dist/roots/flush-coordinator.js +38 -0
  57. package/dist/roots/registry.d.ts +6 -4
  58. package/dist/roots/registry.js +47 -33
  59. package/dist/roots/root.d.ts +32 -2
  60. package/dist/roots/root.js +104 -14
  61. package/dist/roots/settings-equality.d.ts +5 -0
  62. package/dist/roots/settings-equality.js +24 -0
  63. package/dist/roots/touchstrip-root.d.ts +93 -0
  64. package/dist/roots/touchstrip-root.js +383 -0
  65. package/dist/types.d.ts +62 -16
  66. package/dist/vite.d.ts +22 -8
  67. package/dist/vite.js +24 -8
  68. package/package.json +5 -4
  69. package/dist/context/touchbar-context.d.ts +0 -2
  70. package/dist/context/touchbar-context.js +0 -5
  71. package/dist/hooks/touchbar.d.ts +0 -6
  72. package/dist/hooks/touchbar.js +0 -37
  73. package/dist/roots/touchbar-root.d.ts +0 -45
  74. package/dist/roots/touchbar-root.js +0 -175
@@ -1,6 +1,7 @@
1
+ import { metrics } from "../render/metrics.js";
1
2
  import { serializeValue } from "./serialization/value.js";
2
3
  import { serializeVNode } from "./serialization/vnode.js";
3
- import { renderWithHighlight } from "./highlight.js";
4
+ import { renderTouchStripWithHighlight, renderWithHighlight } from "./highlight.js";
4
5
  //#region src/devtools/bridge.ts
5
6
  var RingBuffer = class {
6
7
  items;
@@ -27,7 +28,7 @@ var NETWORK_RING_SIZE = 100;
27
28
  var EVENT_RING_SIZE = 200;
28
29
  var consoleIdCounter = 0;
29
30
  var eventIdCounter = 0;
30
- var DevtoolsBridge = class {
31
+ var DevtoolsBridge = class DevtoolsBridge {
31
32
  server;
32
33
  devtoolsName;
33
34
  renderConfig;
@@ -35,12 +36,14 @@ var DevtoolsBridge = class {
35
36
  networkRing = new RingBuffer(NETWORK_RING_SIZE);
36
37
  eventRing = new RingBuffer(EVENT_RING_SIZE);
37
38
  actions = /* @__PURE__ */ new Map();
38
- touchBars = /* @__PURE__ */ new Map();
39
+ touchStrips = /* @__PURE__ */ new Map();
39
40
  lastRenderSent = /* @__PURE__ */ new Map();
40
41
  pendingTrailing = /* @__PURE__ */ new Map();
41
42
  eventBusOwners = /* @__PURE__ */ new Map();
42
43
  highlightedActionId = null;
43
44
  highlightedNodeId = null;
45
+ _lastProfile = null;
46
+ _metricsTimer = null;
44
47
  constructor(server, devtoolsName, renderConfig) {
45
48
  this.server = server;
46
49
  this.devtoolsName = devtoolsName;
@@ -131,25 +134,25 @@ var DevtoolsBridge = class {
131
134
  this.pendingTrailing.delete(actionId);
132
135
  }
133
136
  }
134
- onTouchBarCreated(deviceId, root, deviceInfo) {
135
- this.touchBars.set(deviceId, {
137
+ onTouchStripCreated(deviceId, root, deviceInfo) {
138
+ this.touchStrips.set(deviceId, {
136
139
  root,
137
140
  deviceInfo,
138
141
  columns: /* @__PURE__ */ new Map()
139
142
  });
140
143
  this.eventBusOwners.set(root.eventBus, {
141
- actionId: `touchbar:${deviceId}`,
144
+ actionId: `touchstrip:${deviceId}`,
142
145
  uuid: ""
143
146
  });
144
147
  }
145
- onTouchBarColumnChanged(deviceId, columns, actionMap) {
146
- const tb = this.touchBars.get(deviceId);
148
+ onTouchStripColumnChanged(deviceId, columns, actionMap) {
149
+ const tb = this.touchStrips.get(deviceId);
147
150
  if (tb) tb.columns = new Map(actionMap);
148
151
  }
149
- onTouchBarDestroyed(deviceId) {
150
- const tb = this.touchBars.get(deviceId);
152
+ onTouchStripDestroyed(deviceId) {
153
+ const tb = this.touchStrips.get(deviceId);
151
154
  if (tb) this.eventBusOwners.delete(tb.root.eventBus);
152
- this.touchBars.delete(deviceId);
155
+ this.touchStrips.delete(deviceId);
153
156
  }
154
157
  onDispatch(actionId, event, payload) {
155
158
  if (!this.server.hasClients()) return;
@@ -207,6 +210,37 @@ var DevtoolsBridge = class {
207
210
  this.consoleRing.push(msg);
208
211
  this.server.broadcast(msg);
209
212
  }
213
+ /**
214
+ * Stash the render profile for the next onRender call.
215
+ * In the pipeline, onProfile fires synchronously before onRender,
216
+ * so the stash is always consumed immediately.
217
+ */
218
+ onProfile(profile) {
219
+ this._lastProfile = profile;
220
+ }
221
+ static METRICS_INTERVAL_MS = 3e3;
222
+ /** Start periodic metrics emission to connected devtools clients. */
223
+ startMetricsEmitter() {
224
+ if (this._metricsTimer) return;
225
+ this._metricsTimer = setInterval(() => {
226
+ if (!this.server.hasClients()) return;
227
+ const snapshot = metrics.snapshot();
228
+ const msg = {
229
+ type: "metrics",
230
+ ts: Date.now(),
231
+ metrics: snapshot
232
+ };
233
+ this.server.broadcast(msg);
234
+ }, DevtoolsBridge.METRICS_INTERVAL_MS);
235
+ if (typeof this._metricsTimer === "object" && "unref" in this._metricsTimer) this._metricsTimer.unref();
236
+ }
237
+ /** Stop periodic metrics emission. */
238
+ stopMetricsEmitter() {
239
+ if (this._metricsTimer) {
240
+ clearInterval(this._metricsTimer);
241
+ this._metricsTimer = null;
242
+ }
243
+ }
210
244
  onFetchRequest(id, method, url, headers, body) {
211
245
  const msg = {
212
246
  type: "network:request",
@@ -247,6 +281,8 @@ var DevtoolsBridge = class {
247
281
  }
248
282
  onRender(container, dataUri) {
249
283
  if (!this.server.hasClients()) return;
284
+ const profile = this._lastProfile;
285
+ this._lastProfile = null;
250
286
  let actionId = null;
251
287
  let meta = null;
252
288
  for (const [id, m] of this.actions) if (m.root.vcontainer === container) {
@@ -256,30 +292,32 @@ var DevtoolsBridge = class {
256
292
  }
257
293
  if (actionId && meta) {
258
294
  meta.root.lastDataUri = dataUri;
259
- this.throttledRender(actionId, container, dataUri, meta);
295
+ this.throttledRender(actionId, container, dataUri, meta, profile);
260
296
  if (this.highlightedActionId === actionId && this.highlightedNodeId !== null) this.applyHighlight(actionId, this.highlightedNodeId, meta).catch(() => {});
261
297
  return;
262
298
  }
263
- for (const [deviceId, tb] of this.touchBars) if (tb.root.vcontainer === container) {
264
- this.emitTouchBarRender(deviceId, tb);
299
+ for (const [deviceId, tb] of this.touchStrips) if (tb.root.vcontainer === container) {
300
+ this.emitTouchStripRender(deviceId, tb, profile);
301
+ const tbActionId = `${DevtoolsBridge.TB_PREFIX}${deviceId}`;
302
+ if (this.highlightedActionId === tbActionId && this.highlightedNodeId !== null) this.applyTouchStripHighlight(tbActionId, deviceId, this.highlightedNodeId, tb).catch(() => {});
265
303
  return;
266
304
  }
267
305
  }
268
- throttledRender(actionId, container, dataUri, meta) {
306
+ throttledRender(actionId, container, dataUri, meta, profile) {
269
307
  const now = Date.now();
270
308
  const elapsed = now - (this.lastRenderSent.get(actionId) ?? 0);
271
309
  const pending = this.pendingTrailing.get(actionId);
272
310
  if (pending) clearTimeout(pending);
273
311
  if (elapsed >= RENDER_THROTTLE_MS) {
274
- this.emitRender(actionId, container, dataUri, meta, now);
312
+ this.emitRender(actionId, container, dataUri, meta, now, profile);
275
313
  this.lastRenderSent.set(actionId, now);
276
314
  } else this.pendingTrailing.set(actionId, setTimeout(() => {
277
- this.emitRender(actionId, container, dataUri, meta, Date.now());
315
+ this.emitRender(actionId, container, dataUri, meta, Date.now(), profile);
278
316
  this.lastRenderSent.set(actionId, Date.now());
279
317
  this.pendingTrailing.delete(actionId);
280
318
  }, RENDER_THROTTLE_MS - elapsed));
281
319
  }
282
- emitRender(actionId, container, dataUri, meta, ts) {
320
+ emitRender(actionId, container, dataUri, meta, ts, profile) {
283
321
  const tree = serializeVNode(container);
284
322
  const msg = {
285
323
  type: "render",
@@ -293,11 +331,27 @@ var DevtoolsBridge = class {
293
331
  },
294
332
  tree,
295
333
  dataUri,
296
- renderMs: 0
334
+ renderMs: profile?.totalMs ?? 0,
335
+ ...profile ? { profile: this.toProfileData(profile) } : {}
297
336
  };
298
337
  this.server.broadcast(msg);
299
338
  }
300
- emitTouchBarRender(deviceId, tb) {
339
+ /** Convert internal RenderProfile to wire-protocol ProfileData. */
340
+ toProfileData(profile) {
341
+ return {
342
+ vnodeToElementMs: profile.vnodeToElementMs,
343
+ fromJsxMs: profile.fromJsxMs,
344
+ takumiRenderMs: profile.takumiRenderMs,
345
+ hashMs: profile.hashMs,
346
+ base64Ms: profile.base64Ms,
347
+ totalMs: profile.totalMs,
348
+ skipped: profile.skipped,
349
+ cacheHit: profile.cacheHit,
350
+ treeDepth: profile.treeDepth,
351
+ nodeCount: profile.nodeCount
352
+ };
353
+ }
354
+ emitTouchStripRender(deviceId, tb, profile) {
301
355
  const tree = serializeVNode(tb.root.vcontainer);
302
356
  const segments = [];
303
357
  for (const [column, actionId] of tb.columns) {
@@ -309,7 +363,7 @@ var DevtoolsBridge = class {
309
363
  });
310
364
  }
311
365
  const msg = {
312
- type: "render:touchbar",
366
+ type: "render:touchstrip",
313
367
  ts: Date.now(),
314
368
  deviceId,
315
369
  canvas: {
@@ -318,46 +372,72 @@ var DevtoolsBridge = class {
318
372
  },
319
373
  tree,
320
374
  segments,
321
- renderMs: 0
375
+ renderMs: profile?.totalMs ?? 0,
376
+ ...profile ? { profile: this.toProfileData(profile) } : {}
322
377
  };
323
378
  this.server.broadcast(msg);
324
379
  }
380
+ static TB_PREFIX = "touchstrip:";
325
381
  async handleHighlight(actionId, nodeId) {
326
382
  try {
327
383
  const prevId = this.highlightedActionId;
328
384
  this.highlightedActionId = actionId;
329
385
  this.highlightedNodeId = nodeId;
330
386
  if (prevId && prevId !== actionId) {
331
- const prevMeta = this.actions.get(prevId);
332
- if (prevMeta) {
333
- prevMeta.root.suppressHardwarePush = false;
334
- if (prevMeta.root.lastDataUri) await prevMeta.root.pushImage(prevMeta.root.lastDataUri).catch(() => {});
335
- }
336
- this.broadcastHighlightRender(prevId, null);
387
+ await this.restoreHighlight(prevId);
388
+ this.broadcastHighlightClear(prevId);
337
389
  }
338
390
  if (!actionId || nodeId === null) {
339
391
  if (actionId && prevId === actionId) {
340
- const meta = this.actions.get(actionId);
341
- if (meta) {
342
- meta.root.suppressHardwarePush = false;
343
- if (meta.root.lastDataUri) await meta.root.pushImage(meta.root.lastDataUri).catch(() => {});
344
- }
345
- this.broadcastHighlightRender(actionId, null);
392
+ await this.restoreHighlight(actionId);
393
+ this.broadcastHighlightClear(actionId);
346
394
  }
347
395
  this.highlightedActionId = null;
348
396
  this.highlightedNodeId = null;
349
397
  return;
350
398
  }
351
- const meta = this.actions.get(actionId);
352
- if (!meta) {
353
- this.highlightedActionId = null;
354
- this.highlightedNodeId = null;
355
- return;
399
+ if (actionId.startsWith(DevtoolsBridge.TB_PREFIX)) {
400
+ const deviceId = actionId.slice(DevtoolsBridge.TB_PREFIX.length);
401
+ const tb = this.touchStrips.get(deviceId);
402
+ if (!tb) {
403
+ this.highlightedActionId = null;
404
+ this.highlightedNodeId = null;
405
+ return;
406
+ }
407
+ tb.root.suppressHardwarePush = true;
408
+ await this.applyTouchStripHighlight(actionId, deviceId, nodeId, tb);
409
+ } else {
410
+ const meta = this.actions.get(actionId);
411
+ if (!meta) {
412
+ this.highlightedActionId = null;
413
+ this.highlightedNodeId = null;
414
+ return;
415
+ }
416
+ meta.root.suppressHardwarePush = true;
417
+ await this.applyHighlight(actionId, nodeId, meta);
356
418
  }
357
- meta.root.suppressHardwarePush = true;
358
- await this.applyHighlight(actionId, nodeId, meta);
359
419
  } catch {}
360
420
  }
421
+ /**
422
+ * Restore a highlighted action or touchstrip to its normal state.
423
+ * Un-suppresses hardware pushes and restores the original image(s).
424
+ */
425
+ async restoreHighlight(id) {
426
+ if (id.startsWith(DevtoolsBridge.TB_PREFIX)) {
427
+ const deviceId = id.slice(DevtoolsBridge.TB_PREFIX.length);
428
+ const tb = this.touchStrips.get(deviceId);
429
+ if (tb) {
430
+ tb.root.suppressHardwarePush = false;
431
+ await tb.root.pushSegmentImages(tb.root.lastSegmentUris);
432
+ }
433
+ } else {
434
+ const prevMeta = this.actions.get(id);
435
+ if (prevMeta) {
436
+ prevMeta.root.suppressHardwarePush = false;
437
+ if (prevMeta.root.lastDataUri) await prevMeta.root.pushImage(prevMeta.root.lastDataUri).catch(() => {});
438
+ }
439
+ }
440
+ }
361
441
  async applyHighlight(actionId, nodeId, meta) {
362
442
  try {
363
443
  const uri = await renderWithHighlight(meta.root.vcontainer, meta.canvas.width, meta.canvas.height, this.renderConfig, nodeId);
@@ -367,6 +447,20 @@ var DevtoolsBridge = class {
367
447
  }
368
448
  } catch {}
369
449
  }
450
+ async applyTouchStripHighlight(actionId, deviceId, nodeId, tb) {
451
+ try {
452
+ const columns = tb.root.columnNumbers;
453
+ if (columns.length === 0) return;
454
+ const segmentWidth = 200;
455
+ const segmentHeight = 100;
456
+ const fullWidth = (Math.max(...columns) + 1) * segmentWidth;
457
+ const result = await renderTouchStripWithHighlight(tb.root.vcontainer, fullWidth, segmentHeight, columns, segmentWidth, this.renderConfig.touchstripImageFormat, this.renderConfig, nodeId);
458
+ if (result && this.highlightedActionId === actionId && this.highlightedNodeId === nodeId) {
459
+ await tb.root.pushSegmentImages(result.segmentUris);
460
+ for (const [col, uri] of result.segmentUris) this.broadcastHighlightRender(`${actionId}:seg:${col}`, uri);
461
+ }
462
+ } catch {}
463
+ }
370
464
  /** Broadcast highlight render image (or null to clear) to devtools UI. */
371
465
  broadcastHighlightRender(actionId, dataUri) {
372
466
  const msg = {
@@ -377,6 +471,18 @@ var DevtoolsBridge = class {
377
471
  };
378
472
  this.server.broadcast(msg);
379
473
  }
474
+ /**
475
+ * Clear highlight URIs for the given actionId.
476
+ * For touchstrip IDs, clears all per-segment keys (touchstrip:*:seg:N).
477
+ * For regular actions, clears the single actionId key.
478
+ */
479
+ broadcastHighlightClear(id) {
480
+ if (id.startsWith(DevtoolsBridge.TB_PREFIX)) {
481
+ const deviceId = id.slice(DevtoolsBridge.TB_PREFIX.length);
482
+ const tb = this.touchStrips.get(deviceId);
483
+ if (tb) for (const col of tb.root.columnNumbers) this.broadcastHighlightRender(`${id}:seg:${col}`, null);
484
+ } else this.broadcastHighlightRender(id, null);
485
+ }
380
486
  buildSnapshot() {
381
487
  const actions = [];
382
488
  for (const [actionId, meta] of this.actions) {
@@ -402,8 +508,8 @@ var DevtoolsBridge = class {
402
508
  dataUri: meta.root.lastDataUri
403
509
  });
404
510
  }
405
- const touchBars = [];
406
- for (const [deviceId, tb] of this.touchBars) {
511
+ const touchStrips = [];
512
+ for (const [deviceId, tb] of this.touchStrips) {
407
513
  let tree = null;
408
514
  try {
409
515
  tree = serializeVNode(tb.root.vcontainer);
@@ -414,7 +520,7 @@ var DevtoolsBridge = class {
414
520
  actionId,
415
521
  dataUri: tb.root.lastSegmentUris.get(column) ?? null
416
522
  });
417
- touchBars.push({
523
+ touchStrips.push({
418
524
  deviceId,
419
525
  deviceName: tb.deviceInfo.name,
420
526
  canvas: {
@@ -429,10 +535,11 @@ var DevtoolsBridge = class {
429
535
  type: "snapshot",
430
536
  ts: Date.now(),
431
537
  actions,
432
- touchBars,
538
+ touchStrips,
433
539
  recentConsole: this.consoleRing.toArray(),
434
540
  recentNetwork: this.networkRing.toArray(),
435
- recentEvents: this.eventRing.toArray()
541
+ recentEvents: this.eventRing.toArray(),
542
+ metrics: metrics.snapshot()
436
543
  };
437
544
  }
438
545
  };
@@ -1,3 +1,9 @@
1
+ import { OutputFormat } from '@takumi-rs/core';
1
2
  import { VContainer } from '../reconciler/vnode';
2
3
  import { RenderConfig } from '../render/pipeline';
3
4
  export declare function renderWithHighlight(container: VContainer, width: number, height: number, config: RenderConfig, targetNid: number): Promise<string | null>;
5
+ export interface TouchStripHighlightResult {
6
+ /** Per-column data URIs for ALL segments. */
7
+ segmentUris: Map<number, string>;
8
+ }
9
+ export declare function renderTouchStripWithHighlight(container: VContainer, fullWidth: number, segmentHeight: number, columns: number[], segmentWidth: number, format: OutputFormat, config: RenderConfig, targetNid: number): Promise<TouchStripHighlightResult | null>;
@@ -1,92 +1,141 @@
1
- import { vnodeToElement } from "../reconciler/vnode.js";
2
- import { bufferToDataUri } from "../render/pipeline.js";
3
- import { createElement } from "react";
4
- import { fromJsx } from "@takumi-rs/helpers/jsx";
1
+ import { bufferToDataUri, buildTakumiRoot } from "../render/pipeline.js";
5
2
  //#region src/devtools/highlight.ts
6
- var HIGHLIGHT_BORDER_COLOR = "rgba(111, 168, 220, 0.85)";
7
- var HIGHLIGHT_BORDER_WIDTH = 2;
8
3
  var HIGHLIGHT_BG = "rgba(111, 168, 220, 0.66)";
9
- var HIGHLIGHT_BOX_SHADOW = `inset 0 0 0 ${HIGHLIGHT_BORDER_WIDTH}px ${HIGHLIGHT_BORDER_COLOR}`;
4
+ var HIGHLIGHT_BOX_SHADOW = `inset 0 0 0 2px rgba(111, 168, 220, 0.85)`;
10
5
  async function renderWithHighlight(container, width, height, config, targetNid) {
11
6
  if (container.children.length === 0) return null;
12
7
  const effectiveNid = resolveTargetNid(container, targetNid);
13
- const rootChildren = container.children.map(vnodeToElement);
14
- const { node, stylesheets } = await fromJsx(createElement("div", { style: {
15
- display: "flex",
16
- position: "relative",
17
- width: "100%",
18
- height: "100%"
19
- } }, ...rootChildren));
20
- const renderOpts = {
8
+ const rootNode = buildTakumiRoot(container);
9
+ const counter = { value: 0 };
10
+ counter.value++;
11
+ const target = findTargetTakumiNode(container.children, rootNode.children ?? [], effectiveNid, counter);
12
+ if (!target) return null;
13
+ injectHighlightOverlay(target);
14
+ return bufferToDataUri(await config.renderer.render(rootNode, {
21
15
  width,
22
16
  height,
23
- stylesheets,
17
+ format: config.imageFormat,
24
18
  devicePixelRatio: config.devicePixelRatio
25
- };
26
- const measured = await config.renderer.measure(node, renderOpts);
19
+ }), config.imageFormat);
20
+ }
21
+ async function renderTouchStripWithHighlight(container, fullWidth, segmentHeight, columns, segmentWidth, format, config, targetNid) {
22
+ if (container.children.length === 0) return null;
23
+ const effectiveNid = resolveTargetNid(container, targetNid);
24
+ const rootNode = buildTakumiRoot(container);
27
25
  const counter = { value: 0 };
28
26
  counter.value++;
29
- const bounds = findTargetBounds(container.children, measured.children, effectiveNid, counter, measured.transform[4], measured.transform[5]);
30
- if (!bounds) return null;
31
- const overlay = createElement("div", { style: {
32
- position: "absolute",
33
- top: bounds.y,
34
- left: bounds.x,
35
- width: bounds.width,
36
- height: bounds.height,
37
- backgroundColor: HIGHLIGHT_BG,
38
- boxShadow: HIGHLIGHT_BOX_SHADOW
39
- } });
40
- const { node: hlNode, stylesheets: hlStylesheets } = await fromJsx(createElement("div", { style: {
41
- display: "flex",
42
- position: "relative",
43
- width: "100%",
44
- height: "100%"
45
- } }, ...rootChildren, overlay));
46
- return bufferToDataUri(await config.renderer.render(hlNode, {
47
- ...renderOpts,
48
- format: config.imageFormat,
49
- stylesheets: hlStylesheets
50
- }), config.imageFormat);
27
+ const target = findTargetTakumiNode(container.children, rootNode.children ?? [], effectiveNid, counter);
28
+ if (!target) return null;
29
+ injectHighlightOverlay(target);
30
+ const segmentUris = /* @__PURE__ */ new Map();
31
+ const segmentPromises = columns.map(async (column) => {
32
+ const clipNode = {
33
+ type: "container",
34
+ style: {
35
+ width: segmentWidth,
36
+ height: segmentHeight,
37
+ overflow: "hidden"
38
+ },
39
+ children: [{
40
+ type: "container",
41
+ style: {
42
+ display: "flex",
43
+ width: fullWidth,
44
+ height: segmentHeight,
45
+ marginLeft: -(column * segmentWidth)
46
+ },
47
+ children: rootNode.children
48
+ }]
49
+ };
50
+ const segBuffer = await config.renderer.render(clipNode, {
51
+ width: segmentWidth,
52
+ height: segmentHeight,
53
+ format,
54
+ devicePixelRatio: config.devicePixelRatio
55
+ });
56
+ segmentUris.set(column, bufferToDataUri(segBuffer, format));
57
+ });
58
+ await Promise.all(segmentPromises);
59
+ return { segmentUris };
51
60
  }
52
- function findTargetBounds(vnodes, measuredChildren, targetNid, counter, parentX, parentY) {
53
- let measuredIdx = 0;
61
+ function injectHighlightOverlay(target) {
62
+ const node = target.node;
63
+ const overlay = {
64
+ type: "container",
65
+ style: {
66
+ position: "absolute",
67
+ top: 0,
68
+ left: 0,
69
+ width: "100%",
70
+ height: "100%",
71
+ backgroundColor: HIGHLIGHT_BG,
72
+ boxShadow: HIGHLIGHT_BOX_SHADOW
73
+ }
74
+ };
75
+ if (node.type === "container") {
76
+ node.style = {
77
+ ...node.style ?? {},
78
+ position: "relative"
79
+ };
80
+ if (!node.children) node.children = [];
81
+ node.children.push(overlay);
82
+ } else {
83
+ const wrapper = {
84
+ type: "container",
85
+ style: { position: "relative" },
86
+ children: [target.node, overlay]
87
+ };
88
+ target.parentChildren[target.indexInParent] = wrapper;
89
+ }
90
+ }
91
+ function findTargetTakumiNode(vnodes, takumiNodes, targetNid, counter) {
92
+ let idx = 0;
54
93
  for (const vnode of vnodes) {
55
94
  const nid = counter.value++;
56
- if (vnode.type === "#text") continue;
57
- const measured = measuredChildren[measuredIdx++];
58
- if (!measured) continue;
59
- const absX = parentX + measured.transform[4];
60
- const absY = parentY + measured.transform[5];
95
+ const takumiNode = takumiNodes[idx];
96
+ idx++;
97
+ if (!takumiNode) continue;
61
98
  if (nid === targetNid) return {
62
- x: absX,
63
- y: absY,
64
- width: measured.width,
65
- height: measured.height
99
+ node: takumiNode,
100
+ parentChildren: takumiNodes,
101
+ indexInParent: idx - 1
66
102
  };
67
- const found = findTargetBounds(vnode.children, measured.children, targetNid, counter, absX, absY);
103
+ if (vnode.type === "#text" || vnode.type === "img") continue;
104
+ if (vnode.type === "svg") {
105
+ advanceCounterThroughSubtree(vnode.children, counter);
106
+ continue;
107
+ }
108
+ const childTakumiNodes = takumiNode.children ?? [];
109
+ const found = findTargetTakumiNode(vnode.children, childTakumiNodes, targetNid, counter);
68
110
  if (found) return found;
69
111
  }
70
112
  return null;
71
113
  }
114
+ function advanceCounterThroughSubtree(vnodes, counter) {
115
+ for (const node of vnodes) {
116
+ counter.value++;
117
+ advanceCounterThroughSubtree(node.children, counter);
118
+ }
119
+ }
72
120
  function resolveTargetNid(container, targetNid) {
73
121
  const counter = { value: 0 };
74
122
  counter.value++;
75
123
  for (const child of container.children) {
76
- const resolved = resolveInSubtree(child, targetNid, counter, 0);
124
+ const resolved = resolveInSubtree(child, targetNid, counter, 0, false);
77
125
  if (resolved !== null) return resolved;
78
126
  }
79
127
  return targetNid;
80
128
  }
81
- function resolveInSubtree(node, targetNid, counter, parentNid) {
129
+ function resolveInSubtree(node, targetNid, counter, parentNid, insideSvg) {
82
130
  const nid = counter.value++;
83
- if (nid === targetNid) return node.type === "#text" ? parentNid : nid;
131
+ if (nid === targetNid) return node.type === "#text" || insideSvg ? parentNid : nid;
84
132
  if (node.type === "#text") return null;
133
+ const childInsideSvg = insideSvg || node.type === "svg";
85
134
  for (const child of node.children) {
86
- const resolved = resolveInSubtree(child, targetNid, counter, nid);
135
+ const resolved = resolveInSubtree(child, targetNid, counter, nid, childInsideSvg);
87
136
  if (resolved !== null) return resolved;
88
137
  }
89
138
  return null;
90
139
  }
91
140
  //#endregion
92
- export { renderWithHighlight };
141
+ export { renderTouchStripWithHighlight, renderWithHighlight };
@@ -11,6 +11,9 @@ function startDevtoolsServer(config) {
11
11
  config.renderConfig.onRender = (container, dataUri) => {
12
12
  bridge.onRender(container, dataUri);
13
13
  };
14
+ config.renderConfig.onProfile = (profile) => {
15
+ bridge.onProfile(profile);
16
+ };
14
17
  EventBus.devtoolsObserver = (bus, event, payload) => {
15
18
  bridge.onEventBusEmit(bus, event, payload);
16
19
  };
@@ -23,12 +26,15 @@ function startDevtoolsServer(config) {
23
26
  onError: (id, error, dur) => bridge.onFetchError(id, error, dur)
24
27
  });
25
28
  server.start();
29
+ bridge.startMetricsEmitter();
26
30
  process.on("exit", () => {
31
+ bridge.stopMetricsEmitter();
27
32
  restoreConsole();
28
33
  restoreFetch();
29
34
  EventBus.devtoolsObserver = null;
30
35
  config.registry.observer = null;
31
36
  config.renderConfig.onRender = void 0;
37
+ config.renderConfig.onProfile = void 0;
32
38
  server.stop();
33
39
  });
34
40
  }
@@ -1,5 +1,5 @@
1
1
  import { ReactRoot } from '../../roots/root';
2
- import { TouchBarRoot } from '../../roots/touchbar-root';
2
+ import { TouchStripRoot } from '../../roots/touchstrip-root';
3
3
  import { CanvasInfo, DeviceInfo } from '../../types';
4
4
  export interface RegistryObserver {
5
5
  onRootCreated(actionId: string, root: ReactRoot, meta: {
@@ -13,8 +13,8 @@ export interface RegistryObserver {
13
13
  };
14
14
  }): void;
15
15
  onRootDestroyed(actionId: string): void;
16
- onTouchBarCreated(deviceId: string, root: TouchBarRoot, deviceInfo: DeviceInfo): void;
17
- onTouchBarColumnChanged(deviceId: string, columns: number[], actionMap: Map<number, string>): void;
18
- onTouchBarDestroyed(deviceId: string): void;
16
+ onTouchStripCreated(deviceId: string, root: TouchStripRoot, deviceInfo: DeviceInfo): void;
17
+ onTouchStripColumnChanged(deviceId: string, columns: number[], actionMap: Map<number, string>): void;
18
+ onTouchStripDestroyed(deviceId: string): void;
19
19
  onDispatch(actionId: string, event: string, payload: unknown): void;
20
20
  }
@@ -35,7 +35,12 @@ export declare class DevtoolsServer {
35
35
  /**
36
36
  * GET /message?d=<json> — fire-and-forget client→server messages.
37
37
  * Used via `new Image().src` to bypass CORS/PNA preflight entirely.
38
- * Returns a 1x1 transparent GIF.
38
+ * Returns a 1x1 transparent GIF (smallest valid image response).
39
+ *
40
+ * Why: browsers enforce Private Network Access (PNA) restrictions
41
+ * on fetch/XHR to localhost from public origins. Image loading
42
+ * is exempt from PNA preflight, so `new Image().src = url` works
43
+ * without triggering CORS errors.
39
44
  */
40
45
  private handleGetMessage;
41
46
  /** POST /message — client→server messages (JSON body). Fallback for programmatic use. */
@@ -153,7 +153,12 @@ var DevtoolsServer = class {
153
153
  /**
154
154
  * GET /message?d=<json> — fire-and-forget client→server messages.
155
155
  * Used via `new Image().src` to bypass CORS/PNA preflight entirely.
156
- * Returns a 1x1 transparent GIF.
156
+ * Returns a 1x1 transparent GIF (smallest valid image response).
157
+ *
158
+ * Why: browsers enforce Private Network Access (PNA) restrictions
159
+ * on fetch/XHR to localhost from public origins. Image loading
160
+ * is exempt from PNA preflight, so `new Image().src = url` works
161
+ * without triggering CORS errors.
157
162
  */
158
163
  handleGetMessage(req, res) {
159
164
  const qs = (req.url ?? "").split("?")[1] ?? "";