@threenative/ui 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -8,6 +8,14 @@ interface IGameCanvasProps<TState extends Record<string, unknown> = Record<strin
8
8
  }
9
9
  declare function GameCanvas<TState extends Record<string, unknown>, TPhysics>({ className, game, }: IGameCanvasProps<TState, TPhysics>): react_jsx_runtime.JSX.Element;
10
10
 
11
+ /**
12
+ * The dev-only gate, and the reason the view below is a separate component.
13
+ *
14
+ * `isDev` folds to `false` in a production build, so this returns null before any hook runs and
15
+ * `DebugOverlayView` becomes unreachable — the bundler then drops the whole view, and with it every
16
+ * reference to the devtools global. A guard placed after the hooks keeps those references alive
17
+ * instead, which `scripts/check-core-boundary.ts` rejects: the overlay must not ship in a game.
18
+ */
11
19
  declare function DebugOverlay(): react_jsx_runtime.JSX.Element | null;
12
20
 
13
21
  type GameSelector<TState, TSelected> = (state: TState) => TSelected;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { createContext, useRef, useState, useEffect, useSyncExternalStore, useMemo, useContext } from 'react';
2
- import { jsx, jsxs } from 'react/jsx-runtime';
3
- import { connectUiBridge, subscribeUiState, publishHitRegions, sendUiIntent, UI_READY_INTENT } from '@threenative/core/ui-layer';
1
+ import { createContext, useRef, useState, useEffect, useCallback, useMemo, useSyncExternalStore, useContext } from 'react';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { connectUiBridge, subscribeUiState, publishHitRegions, sendUiIntent, UI_READY_INTENT, UI_DEV_METRICS_MESSAGE } from '@threenative/core/ui-layer';
4
4
 
5
5
  // src/GameCanvas.tsx
6
6
  function GameCanvas({
@@ -51,45 +51,391 @@ function GameCanvas({
51
51
  );
52
52
  }
53
53
  var isDev = import.meta.env?.DEV === true;
54
+ var ROW_LIMIT = 50;
55
+ var DEFAULT_SMALL_PIXELS = 32;
56
+ var SORTS = ["triangles", "draws", "projected"];
57
+ function devTools() {
58
+ return globalThis.window?.__THREENATIVE__;
59
+ }
54
60
  function readSnapshot() {
55
- const snapshot = globalThis.window?.__THREENATIVE__?.snapshot;
61
+ const snapshot = devTools()?.snapshot;
56
62
  return typeof snapshot === "function" ? snapshot() : {};
57
63
  }
58
64
  function displayValue(value) {
59
65
  return typeof value === "string" ? value : JSON.stringify(value) ?? String(value);
60
66
  }
67
+ function count(value) {
68
+ return value === void 0 ? "\u2014" : Math.round(value).toLocaleString("en-US");
69
+ }
70
+ function sortKey(row, sort) {
71
+ if (sort === "draws") return row.draws;
72
+ if (sort === "projected") return row.projectedPixels;
73
+ return row.submittedTriangles;
74
+ }
75
+ function rank(rows, sort) {
76
+ return [...rows].sort((left, right) => {
77
+ const a = sortKey(left, sort);
78
+ const b = sortKey(right, sort);
79
+ if (a === void 0 && b === void 0) return left.path.localeCompare(right.path);
80
+ if (a === void 0) return 1;
81
+ if (b === void 0) return -1;
82
+ return b - a;
83
+ });
84
+ }
61
85
  function DebugOverlay() {
86
+ if (!isDev) return null;
87
+ return /* @__PURE__ */ jsx(DebugOverlayView, {});
88
+ }
89
+ function DebugOverlayView() {
62
90
  const [open, setOpen] = useState(false);
91
+ const [tab, setTab] = useState("entities");
63
92
  const [snapshot, setSnapshot] = useState({});
93
+ const [report, setReport] = useState(void 0);
94
+ const [capturing, setCapturing] = useState(false);
95
+ const [failure, setFailure] = useState(void 0);
96
+ const [group, setGroup] = useState("objects");
97
+ const [sort, setSort] = useState("triangles");
98
+ const [smallOnly, setSmallOnly] = useState(false);
99
+ const [smallPixels, setSmallPixels] = useState(DEFAULT_SMALL_PIXELS);
100
+ const [expanded, setExpanded] = useState([]);
101
+ const [selected, setSelected] = useState(void 0);
102
+ const [copied, setCopied] = useState(void 0);
64
103
  useEffect(() => {
65
- if (!isDev || typeof window === "undefined") return void 0;
104
+ if (typeof window === "undefined") return void 0;
66
105
  const hostWindow = window;
67
106
  const toggle = (event) => {
68
107
  if (event.key === "`") setOpen((visible) => !visible);
69
108
  };
109
+ const clearOutline = () => setSelected(void 0);
70
110
  hostWindow.addEventListener("keydown", toggle);
111
+ hostWindow.addEventListener("resize", clearOutline);
71
112
  const timer = hostWindow.setInterval(() => setSnapshot(readSnapshot()), 100);
72
113
  return () => {
73
114
  hostWindow.removeEventListener("keydown", toggle);
115
+ hostWindow.removeEventListener("resize", clearOutline);
74
116
  hostWindow.clearInterval(timer);
75
117
  };
76
118
  }, []);
77
- if (!isDev || !open) return null;
78
- const rows = Object.entries(snapshot).flatMap(
119
+ const capture = useCallback(() => {
120
+ const geometry = devTools()?.geometry;
121
+ if (typeof geometry !== "function" || capturing) return;
122
+ setCapturing(true);
123
+ setSelected(void 0);
124
+ setCopied(void 0);
125
+ setFailure(void 0);
126
+ geometry({ limit: ROW_LIMIT, sort }).then((next) => setReport(next)).catch((error) => {
127
+ setReport(void 0);
128
+ setFailure(error instanceof Error ? error.message : String(error));
129
+ }).finally(() => setCapturing(false));
130
+ }, [capturing, sort]);
131
+ const rows = useMemo(() => rank(report?.objects ?? [], sort), [report, sort]);
132
+ const visibleRows = useMemo(
133
+ () => smallOnly ? rows.filter(
134
+ (row) => row.projectedPixels !== void 0 && row.projectedPixels < smallPixels
135
+ ) : rows,
136
+ [rows, smallOnly, smallPixels]
137
+ );
138
+ const selectedRow = rows.find((row) => row.id === selected);
139
+ if (!open) return null;
140
+ const entityRows = Object.entries(snapshot).flatMap(
79
141
  ([entity, fields]) => Object.entries(fields).map(([key, value]) => ({ entity, key, value }))
80
142
  );
81
- return /* @__PURE__ */ jsx("aside", { "aria-label": "ThreeNative entity debug overlay", "data-threenative-debug-overlay": "true", children: /* @__PURE__ */ jsxs("table", { children: [
82
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
83
- /* @__PURE__ */ jsx("th", { children: "entity" }),
84
- /* @__PURE__ */ jsx("th", { children: "key" }),
85
- /* @__PURE__ */ jsx("th", { children: "value" })
86
- ] }) }),
87
- /* @__PURE__ */ jsx("tbody", { children: rows.map(({ entity, key, value }) => /* @__PURE__ */ jsxs("tr", { children: [
88
- /* @__PURE__ */ jsx("td", { children: entity }),
89
- /* @__PURE__ */ jsx("td", { children: key }),
90
- /* @__PURE__ */ jsx("td", { children: displayValue(value) })
91
- ] }, `${entity}.${key}`)) })
92
- ] }) });
143
+ const captureAvailable = typeof devTools()?.geometry === "function";
144
+ return /* @__PURE__ */ jsxs(
145
+ "aside",
146
+ {
147
+ "aria-label": "ThreeNative entity debug overlay",
148
+ "data-threenative-debug-overlay": "true",
149
+ "data-tn-debug-tab": tab,
150
+ children: [
151
+ /* @__PURE__ */ jsxs("div", { className: "tn-debug-tabs", role: "tablist", "aria-label": "Debug views", children: [
152
+ /* @__PURE__ */ jsx(
153
+ "button",
154
+ {
155
+ "aria-controls": "tn-debug-entities",
156
+ "aria-selected": tab === "entities",
157
+ className: "tn-debug-tab",
158
+ id: "tn-debug-entities-tab",
159
+ onClick: () => setTab("entities"),
160
+ role: "tab",
161
+ type: "button",
162
+ children: "Entities"
163
+ }
164
+ ),
165
+ /* @__PURE__ */ jsx(
166
+ "button",
167
+ {
168
+ "aria-controls": "tn-debug-geometry",
169
+ "aria-selected": tab === "geometry",
170
+ className: "tn-debug-tab",
171
+ id: "tn-debug-geometry-tab",
172
+ onClick: () => setTab("geometry"),
173
+ role: "tab",
174
+ type: "button",
175
+ children: "Geometry"
176
+ }
177
+ )
178
+ ] }),
179
+ tab === "entities" ? /* @__PURE__ */ jsx("div", { "aria-labelledby": "tn-debug-entities-tab", id: "tn-debug-entities", role: "tabpanel", children: /* @__PURE__ */ jsxs("table", { children: [
180
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
181
+ /* @__PURE__ */ jsx("th", { children: "entity" }),
182
+ /* @__PURE__ */ jsx("th", { children: "key" }),
183
+ /* @__PURE__ */ jsx("th", { children: "value" })
184
+ ] }) }),
185
+ /* @__PURE__ */ jsx("tbody", { children: entityRows.map(({ entity, key, value }) => /* @__PURE__ */ jsxs("tr", { children: [
186
+ /* @__PURE__ */ jsx("td", { children: entity }),
187
+ /* @__PURE__ */ jsx("td", { children: key }),
188
+ /* @__PURE__ */ jsx("td", { children: displayValue(value) })
189
+ ] }, `${entity}.${key}`)) })
190
+ ] }) }) : /* @__PURE__ */ jsxs("div", { "aria-labelledby": "tn-debug-geometry-tab", id: "tn-debug-geometry", role: "tabpanel", children: [
191
+ /* @__PURE__ */ jsxs("div", { className: "tn-debug-controls", children: [
192
+ /* @__PURE__ */ jsx(
193
+ "button",
194
+ {
195
+ className: "tn-debug-action",
196
+ disabled: capturing || !captureAvailable,
197
+ onClick: capture,
198
+ type: "button",
199
+ children: capturing ? "Capturing\u2026" : report === void 0 ? "Capture" : "Refresh"
200
+ }
201
+ ),
202
+ report === void 0 ? null : /* @__PURE__ */ jsx(
203
+ "button",
204
+ {
205
+ className: "tn-debug-action",
206
+ onClick: () => {
207
+ const json = JSON.stringify(report, null, 2);
208
+ const clipboard = globalThis.navigator?.clipboard;
209
+ if (clipboard === void 0) {
210
+ setCopied(json);
211
+ return;
212
+ }
213
+ clipboard.writeText(json).catch(() => setCopied(json));
214
+ },
215
+ type: "button",
216
+ children: "Copy JSON"
217
+ }
218
+ )
219
+ ] }),
220
+ captureAvailable ? null : /* @__PURE__ */ jsx("p", { className: "tn-debug-note", children: "The running game exposes no geometry capture. Mount a development build to use this view." }),
221
+ failure === void 0 ? null : /* @__PURE__ */ jsx("p", { className: "tn-debug-note", children: failure }),
222
+ report === void 0 || report.status === "unavailable" ? report?.reason === void 0 ? null : /* @__PURE__ */ jsx("p", { className: "tn-debug-note", children: report.reason }) : /* @__PURE__ */ jsxs(Fragment, { children: [
223
+ /* @__PURE__ */ jsxs("p", { className: "tn-debug-note", children: [
224
+ "tick ",
225
+ count(report.tick),
226
+ " \xB7 ",
227
+ report.backend ?? "backend unknown",
228
+ " \xB7",
229
+ " ",
230
+ count(report.viewport?.width),
231
+ "\xD7",
232
+ count(report.viewport?.height),
233
+ " \xB7",
234
+ " ",
235
+ report.camera?.type ?? "camera unknown",
236
+ " \xB7 ",
237
+ (report.durationMs ?? 0).toFixed(1),
238
+ " ms \xB7 ",
239
+ count(report.returned),
240
+ " of ",
241
+ count(report.matched),
242
+ " rows"
243
+ ] }),
244
+ report.partialRanking === true ? /* @__PURE__ */ jsx("p", { className: "tn-debug-warn", children: "Ranking is partial \u2014 not a global claim." }) : null,
245
+ report.rowsTruncated === true ? /* @__PURE__ */ jsxs("p", { className: "tn-debug-warn", children: [
246
+ "Rows were cut by the limit (",
247
+ count(report.limit),
248
+ ")."
249
+ ] }) : null,
250
+ report.inspectionComplete === false ? /* @__PURE__ */ jsxs("p", { className: "tn-debug-warn", children: [
251
+ "Inspection stopped at the walk cap (",
252
+ count(report.inspectedNodes),
253
+ " nodes)."
254
+ ] }) : null,
255
+ sort === report.sort ? null : /* @__PURE__ */ jsxs("p", { className: "tn-debug-note", children: [
256
+ "These rows were ranked by ",
257
+ report.sort,
258
+ "; this table is re-sorted locally."
259
+ ] }),
260
+ /* @__PURE__ */ jsxs("table", { className: "tn-debug-passes", children: [
261
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
262
+ /* @__PURE__ */ jsx("th", { children: "pass" }),
263
+ /* @__PURE__ */ jsx("th", { children: "draws" }),
264
+ /* @__PURE__ */ jsx("th", { children: "triangles" }),
265
+ /* @__PURE__ */ jsx("th", { children: "rows" }),
266
+ /* @__PURE__ */ jsx("th", { children: "unattributed (measured \u2212 rows)" })
267
+ ] }) }),
268
+ /* @__PURE__ */ jsx("tbody", { children: (report.passes ?? []).map((pass) => /* @__PURE__ */ jsxs("tr", { children: [
269
+ /* @__PURE__ */ jsx("td", { children: pass.kind }),
270
+ /* @__PURE__ */ jsx("td", { children: count(pass.draws) }),
271
+ /* @__PURE__ */ jsx("td", { children: count(pass.triangles) }),
272
+ /* @__PURE__ */ jsx("td", { children: count(pass.attributedTriangles) }),
273
+ /* @__PURE__ */ jsx("td", { children: count(pass.unattributedTriangles) })
274
+ ] }, pass.kind)) })
275
+ ] }),
276
+ /* @__PURE__ */ jsxs("fieldset", { className: "tn-debug-controls", children: [
277
+ /* @__PURE__ */ jsx("legend", { children: "Group" }),
278
+ /* @__PURE__ */ jsxs("label", { children: [
279
+ /* @__PURE__ */ jsx(
280
+ "input",
281
+ {
282
+ checked: group === "objects",
283
+ name: "tn-debug-group",
284
+ onChange: () => setGroup("objects"),
285
+ type: "radio"
286
+ }
287
+ ),
288
+ "Objects"
289
+ ] }),
290
+ /* @__PURE__ */ jsxs("label", { children: [
291
+ /* @__PURE__ */ jsx(
292
+ "input",
293
+ {
294
+ checked: group === "assets",
295
+ name: "tn-debug-group",
296
+ onChange: () => setGroup("assets"),
297
+ type: "radio"
298
+ }
299
+ ),
300
+ "Assets"
301
+ ] })
302
+ ] }),
303
+ /* @__PURE__ */ jsxs("div", { className: "tn-debug-controls", children: [
304
+ /* @__PURE__ */ jsxs("label", { children: [
305
+ "Sort",
306
+ /* @__PURE__ */ jsx(
307
+ "select",
308
+ {
309
+ onChange: (event) => setSort(event.target.value),
310
+ value: sort,
311
+ children: SORTS.map((option) => /* @__PURE__ */ jsx("option", { value: option, children: option }, option))
312
+ }
313
+ )
314
+ ] }),
315
+ /* @__PURE__ */ jsxs("label", { children: [
316
+ /* @__PURE__ */ jsx(
317
+ "input",
318
+ {
319
+ checked: smallOnly,
320
+ onChange: (event) => setSmallOnly(event.target.checked),
321
+ type: "checkbox"
322
+ }
323
+ ),
324
+ "Small on screen (< ",
325
+ count(smallPixels),
326
+ " px)"
327
+ ] }),
328
+ /* @__PURE__ */ jsxs("label", { children: [
329
+ "Threshold",
330
+ /* @__PURE__ */ jsx(
331
+ "input",
332
+ {
333
+ min: 1,
334
+ onChange: (event) => setSmallPixels(Math.max(1, Number(event.target.value) || 1)),
335
+ type: "number",
336
+ value: smallPixels
337
+ }
338
+ )
339
+ ] })
340
+ ] }),
341
+ /* @__PURE__ */ jsx("p", { className: "tn-debug-note", children: "A diagnostic filter, not a recommendation: whether an object matters is an authoring decision, and an object with no measured bounds is excluded rather than called small." }),
342
+ group === "assets" ? /* @__PURE__ */ jsxs("table", { className: "tn-debug-assets", children: [
343
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
344
+ /* @__PURE__ */ jsx("th", { children: "asset" }),
345
+ /* @__PURE__ */ jsx("th", { children: "objects" }),
346
+ /* @__PURE__ */ jsx("th", { children: "triangles" }),
347
+ /* @__PURE__ */ jsx("th", { children: "unique" }),
348
+ /* @__PURE__ */ jsx("th", { children: "draws" })
349
+ ] }) }),
350
+ /* @__PURE__ */ jsx("tbody", { children: (report.assets ?? []).map((asset) => /* @__PURE__ */ jsxs("tr", { children: [
351
+ /* @__PURE__ */ jsx("td", { children: asset.asset }),
352
+ /* @__PURE__ */ jsx("td", { children: count(asset.objects) }),
353
+ /* @__PURE__ */ jsx("td", { children: count(asset.submittedTriangles) }),
354
+ /* @__PURE__ */ jsx("td", { children: count(asset.uniqueTriangles) }),
355
+ /* @__PURE__ */ jsx("td", { children: count(asset.draws) })
356
+ ] }, asset.asset)) })
357
+ ] }) : /* @__PURE__ */ jsxs("table", { className: "tn-debug-objects", children: [
358
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
359
+ /* @__PURE__ */ jsx("th", { children: "object" }),
360
+ /* @__PURE__ */ jsx("th", { children: "triangles" }),
361
+ /* @__PURE__ */ jsx("th", { children: "draws" }),
362
+ /* @__PURE__ */ jsx("th", { children: "projected px" }),
363
+ /* @__PURE__ */ jsx("th", { children: "copies" }),
364
+ /* @__PURE__ */ jsx("th", { children: "asset" })
365
+ ] }) }),
366
+ /* @__PURE__ */ jsx("tbody", { children: visibleRows.flatMap((row) => {
367
+ const isExpanded = expanded.includes(row.id);
368
+ const head = /* @__PURE__ */ jsxs("tr", { title: (row.unavailable ?? []).join(" "), children: [
369
+ /* @__PURE__ */ jsxs("td", { children: [
370
+ /* @__PURE__ */ jsx(
371
+ "button",
372
+ {
373
+ "aria-expanded": isExpanded,
374
+ className: "tn-debug-expand",
375
+ onClick: () => setExpanded(
376
+ (current) => current.includes(row.id) ? current.filter((id) => id !== row.id) : [...current, row.id]
377
+ ),
378
+ type: "button",
379
+ children: isExpanded ? "\u2212" : "+"
380
+ }
381
+ ),
382
+ /* @__PURE__ */ jsx(
383
+ "button",
384
+ {
385
+ "aria-pressed": selected === row.id,
386
+ className: "tn-debug-select",
387
+ onClick: () => setSelected((current) => current === row.id ? void 0 : row.id),
388
+ type: "button",
389
+ children: row.name
390
+ }
391
+ ),
392
+ (row.unavailable ?? []).length === 0 ? null : /* @__PURE__ */ jsx("small", { className: "tn-debug-unavailable", children: (row.unavailable ?? []).join(" ") })
393
+ ] }),
394
+ /* @__PURE__ */ jsxs("td", { children: [
395
+ count(row.submittedTriangles),
396
+ row.trianglesSource === "batchMembers" ? " (derived)" : ""
397
+ ] }),
398
+ /* @__PURE__ */ jsx("td", { children: count(row.draws) }),
399
+ /* @__PURE__ */ jsx("td", { children: count(row.projectedPixels) }),
400
+ /* @__PURE__ */ jsx("td", { children: count(row.copies) }),
401
+ /* @__PURE__ */ jsx("td", { children: row.asset ?? "\u2014" })
402
+ ] }, row.id);
403
+ if (!isExpanded) return [head];
404
+ return [
405
+ head,
406
+ ...row.meshes.map((mesh) => /* @__PURE__ */ jsxs("tr", { className: "tn-debug-mesh", children: [
407
+ /* @__PURE__ */ jsx("td", { children: mesh.name }),
408
+ /* @__PURE__ */ jsx("td", { children: count(mesh.submittedTriangles) }),
409
+ /* @__PURE__ */ jsx("td", { children: count(mesh.draws) }),
410
+ /* @__PURE__ */ jsx("td", { children: count(mesh.materials) }),
411
+ /* @__PURE__ */ jsx("td", { children: count(mesh.instances) }),
412
+ /* @__PURE__ */ jsx("td", { children: mesh.batchOwner ?? "\u2014" })
413
+ ] }, mesh.id))
414
+ ];
415
+ }) })
416
+ ] }),
417
+ selectedRow === void 0 ? null : selectedRow.projectedCenter === void 0 || selectedRow.projectedPixels === void 0 || report.viewport === void 0 ? /* @__PURE__ */ jsx("p", { className: "tn-debug-note", children: "bounds unavailable" }) : (
418
+ // Percentages of the captured drawing buffer, which is the canvas the overlay sits
419
+ // over. The box is the capture's ESTIMATED bounds, not a silhouette or a pixel mask.
420
+ /* @__PURE__ */ jsx("div", { className: "tn-debug-outline-layer", children: /* @__PURE__ */ jsx(
421
+ "div",
422
+ {
423
+ className: "tn-debug-outline",
424
+ style: {
425
+ height: `${String(selectedRow.projectedPixels / report.viewport.height * 100)}%`,
426
+ left: `${String((selectedRow.projectedCenter[0] - selectedRow.projectedPixels / 2) / report.viewport.width * 100)}%`,
427
+ top: `${String((selectedRow.projectedCenter[1] - selectedRow.projectedPixels / 2) / report.viewport.height * 100)}%`,
428
+ width: `${String(selectedRow.projectedPixels / report.viewport.width * 100)}%`
429
+ }
430
+ }
431
+ ) })
432
+ ),
433
+ copied === void 0 ? null : /* @__PURE__ */ jsx("textarea", { className: "tn-debug-json", readOnly: true, rows: 8, value: copied })
434
+ ] })
435
+ ] })
436
+ ]
437
+ }
438
+ );
93
439
  }
94
440
  function useGameState(game, selector = (state) => state) {
95
441
  return useSyncExternalStore(
@@ -106,6 +452,43 @@ function useUiLayer(name) {
106
452
  }
107
453
  return value;
108
454
  }
455
+ function DevSceneVerdict({ bridge }) {
456
+ const [verdict, setVerdict] = useState(void 0);
457
+ useEffect(
458
+ () => bridge.onMessage((message) => {
459
+ if (message.type !== UI_DEV_METRICS_MESSAGE) return;
460
+ const warning = message.sceneWarning;
461
+ if (typeof warning === "string" && warning.length > 0) setVerdict(warning);
462
+ }),
463
+ [bridge]
464
+ );
465
+ if (verdict === void 0) return null;
466
+ return (
467
+ // `<output>` carries the status role natively, so the readout is announced without an ARIA
468
+ // attribute standing in for an element that already means this.
469
+ /* @__PURE__ */ jsx(
470
+ "output",
471
+ {
472
+ className: "tn-dev-scene-warning",
473
+ style: {
474
+ position: "absolute",
475
+ top: 8,
476
+ right: 8,
477
+ maxWidth: 420,
478
+ padding: "2px 6px",
479
+ borderRadius: 4,
480
+ background: "rgba(0, 0, 0, 0.55)",
481
+ color: "#ffd98a",
482
+ font: "500 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace",
483
+ whiteSpace: "normal",
484
+ pointerEvents: "none",
485
+ zIndex: 2147483647
486
+ },
487
+ children: verdict
488
+ }
489
+ )
490
+ );
491
+ }
109
492
  function UiLayer({ children }) {
110
493
  const [value, setValue] = useState(void 0);
111
494
  const registryRef = useRef(void 0);
@@ -131,7 +514,10 @@ function UiLayer({ children }) {
131
514
  };
132
515
  }, [value]);
133
516
  if (value === void 0) return null;
134
- return /* @__PURE__ */ jsx(UiLayerContext.Provider, { value, children });
517
+ return /* @__PURE__ */ jsxs(UiLayerContext.Provider, { value, children: [
518
+ children,
519
+ /* @__PURE__ */ jsx(DevSceneVerdict, { bridge: value.bridge })
520
+ ] });
135
521
  }
136
522
  function useUiState(selector) {
137
523
  const { mirror } = useUiLayer("useUiState");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threenative/ui",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "license": "MIT",
5
5
  "description": "React bindings for mounting a ThreeNative game and reading throttled state.",
6
6
  "keywords": [
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "sideEffects": false,
43
43
  "peerDependencies": {
44
- "@threenative/core": ">=0.3.2 <0.4.0",
44
+ "@threenative/core": ">=0.3.4 <0.4.0",
45
45
  "react": ">=19.2.0 <20",
46
46
  "react-dom": ">=19.2.0 <20"
47
47
  },
@@ -55,7 +55,7 @@
55
55
  "react-test-renderer": "19.2.0",
56
56
  "tsup": "8.5.1",
57
57
  "typescript": "5.9.3",
58
- "@threenative/core": "0.3.2"
58
+ "@threenative/core": "0.3.4"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "tsup --config tsup.config.ts && publint",