@three-flatland/devtools 0.1.0-alpha.3

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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +82 -0
  3. package/dist/buffers-modal.cjs +491 -0
  4. package/dist/buffers-modal.cjs.map +1 -0
  5. package/dist/buffers-modal.d.cts +62 -0
  6. package/dist/buffers-modal.d.ts +62 -0
  7. package/dist/buffers-modal.js +467 -0
  8. package/dist/buffers-modal.js.map +1 -0
  9. package/dist/buffers-view.cjs +269 -0
  10. package/dist/buffers-view.cjs.map +1 -0
  11. package/dist/buffers-view.d.cts +39 -0
  12. package/dist/buffers-view.d.ts +39 -0
  13. package/dist/buffers-view.js +245 -0
  14. package/dist/buffers-view.js.map +1 -0
  15. package/dist/create-pane.cjs +203 -0
  16. package/dist/create-pane.cjs.map +1 -0
  17. package/dist/create-pane.d.cts +66 -0
  18. package/dist/create-pane.d.ts +66 -0
  19. package/dist/create-pane.js +178 -0
  20. package/dist/create-pane.js.map +1 -0
  21. package/dist/dashboard/app.tsx +42 -0
  22. package/dist/dashboard/client.ts +21 -0
  23. package/dist/dashboard/export.ts +84 -0
  24. package/dist/dashboard/hooks.ts +75 -0
  25. package/dist/dashboard/index.html +1149 -0
  26. package/dist/dashboard/index.tsx +18 -0
  27. package/dist/dashboard/panels/batches.tsx +282 -0
  28. package/dist/dashboard/panels/buffers.tsx +425 -0
  29. package/dist/dashboard/panels/env.tsx +85 -0
  30. package/dist/dashboard/panels/header-stats.tsx +30 -0
  31. package/dist/dashboard/panels/producer-select.tsx +81 -0
  32. package/dist/dashboard/panels/protocol-log.tsx +743 -0
  33. package/dist/dashboard/panels/registry.tsx +642 -0
  34. package/dist/dashboard/panels/stats.tsx +431 -0
  35. package/dist/dashboard/protocol-store.ts +312 -0
  36. package/dist/dashboard/tsconfig.json +19 -0
  37. package/dist/dashboard/vendor/hooks.module.js +1 -0
  38. package/dist/dashboard/vendor/jsx-runtime.js +22 -0
  39. package/dist/dashboard/vendor/preact.module.js +12 -0
  40. package/dist/devtools-client.cjs +870 -0
  41. package/dist/devtools-client.cjs.map +1 -0
  42. package/dist/devtools-client.d.cts +362 -0
  43. package/dist/devtools-client.d.ts +362 -0
  44. package/dist/devtools-client.js +852 -0
  45. package/dist/devtools-client.js.map +1 -0
  46. package/dist/index.cjs +42 -0
  47. package/dist/index.cjs.map +1 -0
  48. package/dist/index.d.cts +8 -0
  49. package/dist/index.d.ts +8 -0
  50. package/dist/index.js +13 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/perf-trace.cjs +103 -0
  53. package/dist/perf-trace.cjs.map +1 -0
  54. package/dist/perf-trace.d.cts +27 -0
  55. package/dist/perf-trace.d.ts +27 -0
  56. package/dist/perf-trace.js +79 -0
  57. package/dist/perf-trace.js.map +1 -0
  58. package/dist/plugins.cjs +44 -0
  59. package/dist/plugins.cjs.map +1 -0
  60. package/dist/plugins.d.cts +7 -0
  61. package/dist/plugins.d.ts +7 -0
  62. package/dist/plugins.js +9 -0
  63. package/dist/plugins.js.map +1 -0
  64. package/dist/provider-switcher.cjs +95 -0
  65. package/dist/provider-switcher.cjs.map +1 -0
  66. package/dist/provider-switcher.d.cts +18 -0
  67. package/dist/provider-switcher.d.ts +18 -0
  68. package/dist/provider-switcher.js +71 -0
  69. package/dist/provider-switcher.js.map +1 -0
  70. package/dist/react/devtools-provider.cjs +59 -0
  71. package/dist/react/devtools-provider.cjs.map +1 -0
  72. package/dist/react/devtools-provider.d.cts +39 -0
  73. package/dist/react/devtools-provider.d.ts +39 -0
  74. package/dist/react/devtools-provider.js +38 -0
  75. package/dist/react/devtools-provider.js.map +1 -0
  76. package/dist/react/use-fps-graph.cjs +49 -0
  77. package/dist/react/use-fps-graph.cjs.map +1 -0
  78. package/dist/react/use-fps-graph.d.cts +15 -0
  79. package/dist/react/use-fps-graph.d.ts +15 -0
  80. package/dist/react/use-fps-graph.js +25 -0
  81. package/dist/react/use-fps-graph.js.map +1 -0
  82. package/dist/react/use-pane-button.cjs +58 -0
  83. package/dist/react/use-pane-button.cjs.map +1 -0
  84. package/dist/react/use-pane-button.d.cts +10 -0
  85. package/dist/react/use-pane-button.d.ts +10 -0
  86. package/dist/react/use-pane-button.js +34 -0
  87. package/dist/react/use-pane-button.js.map +1 -0
  88. package/dist/react/use-pane-folder.cjs +48 -0
  89. package/dist/react/use-pane-folder.cjs.map +1 -0
  90. package/dist/react/use-pane-folder.d.cts +15 -0
  91. package/dist/react/use-pane-folder.d.ts +15 -0
  92. package/dist/react/use-pane-folder.js +24 -0
  93. package/dist/react/use-pane-folder.js.map +1 -0
  94. package/dist/react/use-pane-input.cjs +70 -0
  95. package/dist/react/use-pane-input.cjs.map +1 -0
  96. package/dist/react/use-pane-input.d.cts +44 -0
  97. package/dist/react/use-pane-input.d.ts +44 -0
  98. package/dist/react/use-pane-input.js +46 -0
  99. package/dist/react/use-pane-input.js.map +1 -0
  100. package/dist/react/use-pane-radio-grid.cjs +78 -0
  101. package/dist/react/use-pane-radio-grid.cjs.map +1 -0
  102. package/dist/react/use-pane-radio-grid.d.cts +35 -0
  103. package/dist/react/use-pane-radio-grid.d.ts +35 -0
  104. package/dist/react/use-pane-radio-grid.js +54 -0
  105. package/dist/react/use-pane-radio-grid.js.map +1 -0
  106. package/dist/react/use-pane.cjs +45 -0
  107. package/dist/react/use-pane.cjs.map +1 -0
  108. package/dist/react/use-pane.d.cts +40 -0
  109. package/dist/react/use-pane.d.ts +40 -0
  110. package/dist/react/use-pane.js +21 -0
  111. package/dist/react/use-pane.js.map +1 -0
  112. package/dist/react.cjs +50 -0
  113. package/dist/react.cjs.map +1 -0
  114. package/dist/react.d.cts +12 -0
  115. package/dist/react.d.ts +12 -0
  116. package/dist/react.js +19 -0
  117. package/dist/react.js.map +1 -0
  118. package/dist/registry-view.cjs +371 -0
  119. package/dist/registry-view.cjs.map +1 -0
  120. package/dist/registry-view.d.cts +24 -0
  121. package/dist/registry-view.d.ts +24 -0
  122. package/dist/registry-view.js +347 -0
  123. package/dist/registry-view.js.map +1 -0
  124. package/dist/stats-graph.cjs +297 -0
  125. package/dist/stats-graph.cjs.map +1 -0
  126. package/dist/stats-graph.d.cts +40 -0
  127. package/dist/stats-graph.d.ts +40 -0
  128. package/dist/stats-graph.js +273 -0
  129. package/dist/stats-graph.js.map +1 -0
  130. package/dist/stats-row.cjs +153 -0
  131. package/dist/stats-row.cjs.map +1 -0
  132. package/dist/stats-row.d.cts +24 -0
  133. package/dist/stats-row.d.ts +24 -0
  134. package/dist/stats-row.js +129 -0
  135. package/dist/stats-row.js.map +1 -0
  136. package/dist/theme.cjs +260 -0
  137. package/dist/theme.cjs.map +1 -0
  138. package/dist/theme.d.cts +10 -0
  139. package/dist/theme.d.ts +10 -0
  140. package/dist/theme.js +235 -0
  141. package/dist/theme.js.map +1 -0
  142. package/dist/vite-plugin.cjs +113 -0
  143. package/dist/vite-plugin.cjs.map +1 -0
  144. package/dist/vite-plugin.d.cts +9 -0
  145. package/dist/vite-plugin.d.ts +9 -0
  146. package/dist/vite-plugin.js +88 -0
  147. package/dist/vite-plugin.js.map +1 -0
  148. package/package.json +124 -0
@@ -0,0 +1,34 @@
1
+ import { useEffect, useRef } from "react";
2
+ function usePaneButton(parent, title, onClick) {
3
+ const callbackRef = useRef(onClick);
4
+ callbackRef.current = onClick;
5
+ const buttonRef = useRef(null);
6
+ const mountedRef = useRef(false);
7
+ if (parent && buttonRef.current === null) {
8
+ const button = parent.addButton({ title });
9
+ button.on("click", () => callbackRef.current());
10
+ buttonRef.current = button;
11
+ }
12
+ useEffect(() => {
13
+ mountedRef.current = true;
14
+ if (parent && buttonRef.current === null) {
15
+ const button = parent.addButton({ title });
16
+ button.on("click", () => callbackRef.current());
17
+ buttonRef.current = button;
18
+ }
19
+ return () => {
20
+ const button = buttonRef.current;
21
+ setTimeout(() => {
22
+ if (!mountedRef.current && button) {
23
+ button.dispose();
24
+ if (buttonRef.current === button) buttonRef.current = null;
25
+ }
26
+ }, 0);
27
+ mountedRef.current = false;
28
+ };
29
+ }, [parent]);
30
+ }
31
+ export {
32
+ usePaneButton
33
+ };
34
+ //# sourceMappingURL=use-pane-button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/use-pane-button.ts"],"sourcesContent":["import { useEffect, useRef } from 'react'\nimport type { PaneParent } from './use-pane-input.js'\n\n/**\n * Add a button to a Tweakpane parent. Uses a ref for the callback to avoid stale closures.\n * Uses deferred disposal to survive React strict mode.\n */\nexport function usePaneButton(\n parent: PaneParent | null,\n title: string,\n onClick: () => void,\n): void {\n const callbackRef = useRef(onClick)\n callbackRef.current = onClick\n const buttonRef = useRef<{ dispose(): void } | null>(null)\n const mountedRef = useRef(false)\n\n if (parent && buttonRef.current === null) {\n const button = parent.addButton({ title })\n button.on('click', () => callbackRef.current())\n buttonRef.current = button\n }\n\n useEffect(() => {\n mountedRef.current = true\n\n if (parent && buttonRef.current === null) {\n const button = parent.addButton({ title })\n button.on('click', () => callbackRef.current())\n buttonRef.current = button\n }\n\n return () => {\n const button = buttonRef.current\n setTimeout(() => {\n if (!mountedRef.current && button) {\n button.dispose()\n if (buttonRef.current === button) buttonRef.current = null\n }\n }, 0)\n mountedRef.current = false\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [parent])\n}\n"],"mappings":"AAAA,SAAS,WAAW,cAAc;AAO3B,SAAS,cACd,QACA,OACA,SACM;AACN,QAAM,cAAc,OAAO,OAAO;AAClC,cAAY,UAAU;AACtB,QAAM,YAAY,OAAmC,IAAI;AACzD,QAAM,aAAa,OAAO,KAAK;AAE/B,MAAI,UAAU,UAAU,YAAY,MAAM;AACxC,UAAM,SAAS,OAAO,UAAU,EAAE,MAAM,CAAC;AACzC,WAAO,GAAG,SAAS,MAAM,YAAY,QAAQ,CAAC;AAC9C,cAAU,UAAU;AAAA,EACtB;AAEA,YAAU,MAAM;AACd,eAAW,UAAU;AAErB,QAAI,UAAU,UAAU,YAAY,MAAM;AACxC,YAAM,SAAS,OAAO,UAAU,EAAE,MAAM,CAAC;AACzC,aAAO,GAAG,SAAS,MAAM,YAAY,QAAQ,CAAC;AAC9C,gBAAU,UAAU;AAAA,IACtB;AAEA,WAAO,MAAM;AACX,YAAM,SAAS,UAAU;AACzB,iBAAW,MAAM;AACf,YAAI,CAAC,WAAW,WAAW,QAAQ;AACjC,iBAAO,QAAQ;AACf,cAAI,UAAU,YAAY,OAAQ,WAAU,UAAU;AAAA,QACxD;AAAA,MACF,GAAG,CAAC;AACJ,iBAAW,UAAU;AAAA,IACvB;AAAA,EAEF,GAAG,CAAC,MAAM,CAAC;AACb;","names":[]}
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var use_pane_folder_exports = {};
20
+ __export(use_pane_folder_exports, {
21
+ usePaneFolder: () => usePaneFolder
22
+ });
23
+ module.exports = __toCommonJS(use_pane_folder_exports);
24
+ var import_react = require("react");
25
+ function usePaneFolder(parent, title, options = {}) {
26
+ const [folder, setFolder] = (0, import_react.useState)(null);
27
+ const getOptions = (0, import_react.useEffectEvent)(() => options);
28
+ (0, import_react.useLayoutEffect)(() => {
29
+ if (!parent) {
30
+ setFolder(null);
31
+ return;
32
+ }
33
+ const f = parent.addFolder({ expanded: false, ...getOptions(), title });
34
+ setFolder(f);
35
+ return () => {
36
+ try {
37
+ f.dispose();
38
+ } catch {
39
+ }
40
+ };
41
+ }, [parent, title]);
42
+ return folder;
43
+ }
44
+ // Annotate the CommonJS export names for ESM import in node:
45
+ 0 && (module.exports = {
46
+ usePaneFolder
47
+ });
48
+ //# sourceMappingURL=use-pane-folder.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/use-pane-folder.ts"],"sourcesContent":["import { useEffectEvent, useLayoutEffect, useState } from 'react'\nimport type { FolderApi, FolderParams } from 'tweakpane'\nimport type { PaneParent } from './use-pane-input.js'\n\n/**\n * Create a Tweakpane folder underneath `parent`. Disposes on unmount and\n * recreates when `parent` (or `title`) changes — e.g., when `usePane`\n * tears down and rebuilds its bundle on a StrictMode remount.\n *\n * Returns `null` on the first render before the layout effect commits,\n * then the FolderApi on the next render. The two renders happen\n * synchronously before paint, so consumers don't see a flicker.\n */\nexport function usePaneFolder(\n parent: PaneParent | null,\n title: string,\n options: Partial<FolderParams> = {},\n): FolderApi | null {\n const [folder, setFolder] = useState<FolderApi | null>(null)\n\n // Read the latest `options` at (re)create time without making them a\n // dependency — useEffectEvent captures current props non-reactively, so\n // changing options doesn't tear down and rebuild the folder.\n const getOptions = useEffectEvent(() => options)\n\n useLayoutEffect(() => {\n if (!parent) {\n setFolder(null)\n return\n }\n const f = parent.addFolder({ expanded: false, ...getOptions(), title })\n setFolder(f)\n return () => {\n try {\n f.dispose()\n } catch {\n // Parent may have been disposed first (cascade), in which case\n // the folder is already gone. Ignore.\n }\n }\n }, [parent, title])\n\n return folder\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA0D;AAanD,SAAS,cACd,QACA,OACA,UAAiC,CAAC,GAChB;AAClB,QAAM,CAAC,QAAQ,SAAS,QAAI,uBAA2B,IAAI;AAK3D,QAAM,iBAAa,6BAAe,MAAM,OAAO;AAE/C,oCAAgB,MAAM;AACpB,QAAI,CAAC,QAAQ;AACX,gBAAU,IAAI;AACd;AAAA,IACF;AACA,UAAM,IAAI,OAAO,UAAU,EAAE,UAAU,OAAO,GAAG,WAAW,GAAG,MAAM,CAAC;AACtE,cAAU,CAAC;AACX,WAAO,MAAM;AACX,UAAI;AACF,UAAE,QAAQ;AAAA,MACZ,QAAQ;AAAA,MAGR;AAAA,IACF;AAAA,EACF,GAAG,CAAC,QAAQ,KAAK,CAAC;AAElB,SAAO;AACT;","names":[]}
@@ -0,0 +1,15 @@
1
+ import { FolderParams, FolderApi } from 'tweakpane';
2
+ import { PaneParent } from './use-pane-input.cjs';
3
+
4
+ /**
5
+ * Create a Tweakpane folder underneath `parent`. Disposes on unmount and
6
+ * recreates when `parent` (or `title`) changes — e.g., when `usePane`
7
+ * tears down and rebuilds its bundle on a StrictMode remount.
8
+ *
9
+ * Returns `null` on the first render before the layout effect commits,
10
+ * then the FolderApi on the next render. The two renders happen
11
+ * synchronously before paint, so consumers don't see a flicker.
12
+ */
13
+ declare function usePaneFolder(parent: PaneParent | null, title: string, options?: Partial<FolderParams>): FolderApi | null;
14
+
15
+ export { usePaneFolder };
@@ -0,0 +1,15 @@
1
+ import { FolderParams, FolderApi } from 'tweakpane';
2
+ import { PaneParent } from './use-pane-input.js';
3
+
4
+ /**
5
+ * Create a Tweakpane folder underneath `parent`. Disposes on unmount and
6
+ * recreates when `parent` (or `title`) changes — e.g., when `usePane`
7
+ * tears down and rebuilds its bundle on a StrictMode remount.
8
+ *
9
+ * Returns `null` on the first render before the layout effect commits,
10
+ * then the FolderApi on the next render. The two renders happen
11
+ * synchronously before paint, so consumers don't see a flicker.
12
+ */
13
+ declare function usePaneFolder(parent: PaneParent | null, title: string, options?: Partial<FolderParams>): FolderApi | null;
14
+
15
+ export { usePaneFolder };
@@ -0,0 +1,24 @@
1
+ import { useEffectEvent, useLayoutEffect, useState } from "react";
2
+ function usePaneFolder(parent, title, options = {}) {
3
+ const [folder, setFolder] = useState(null);
4
+ const getOptions = useEffectEvent(() => options);
5
+ useLayoutEffect(() => {
6
+ if (!parent) {
7
+ setFolder(null);
8
+ return;
9
+ }
10
+ const f = parent.addFolder({ expanded: false, ...getOptions(), title });
11
+ setFolder(f);
12
+ return () => {
13
+ try {
14
+ f.dispose();
15
+ } catch {
16
+ }
17
+ };
18
+ }, [parent, title]);
19
+ return folder;
20
+ }
21
+ export {
22
+ usePaneFolder
23
+ };
24
+ //# sourceMappingURL=use-pane-folder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/use-pane-folder.ts"],"sourcesContent":["import { useEffectEvent, useLayoutEffect, useState } from 'react'\nimport type { FolderApi, FolderParams } from 'tweakpane'\nimport type { PaneParent } from './use-pane-input.js'\n\n/**\n * Create a Tweakpane folder underneath `parent`. Disposes on unmount and\n * recreates when `parent` (or `title`) changes — e.g., when `usePane`\n * tears down and rebuilds its bundle on a StrictMode remount.\n *\n * Returns `null` on the first render before the layout effect commits,\n * then the FolderApi on the next render. The two renders happen\n * synchronously before paint, so consumers don't see a flicker.\n */\nexport function usePaneFolder(\n parent: PaneParent | null,\n title: string,\n options: Partial<FolderParams> = {},\n): FolderApi | null {\n const [folder, setFolder] = useState<FolderApi | null>(null)\n\n // Read the latest `options` at (re)create time without making them a\n // dependency — useEffectEvent captures current props non-reactively, so\n // changing options doesn't tear down and rebuild the folder.\n const getOptions = useEffectEvent(() => options)\n\n useLayoutEffect(() => {\n if (!parent) {\n setFolder(null)\n return\n }\n const f = parent.addFolder({ expanded: false, ...getOptions(), title })\n setFolder(f)\n return () => {\n try {\n f.dispose()\n } catch {\n // Parent may have been disposed first (cascade), in which case\n // the folder is already gone. Ignore.\n }\n }\n }, [parent, title])\n\n return folder\n}\n"],"mappings":"AAAA,SAAS,gBAAgB,iBAAiB,gBAAgB;AAanD,SAAS,cACd,QACA,OACA,UAAiC,CAAC,GAChB;AAClB,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA2B,IAAI;AAK3D,QAAM,aAAa,eAAe,MAAM,OAAO;AAE/C,kBAAgB,MAAM;AACpB,QAAI,CAAC,QAAQ;AACX,gBAAU,IAAI;AACd;AAAA,IACF;AACA,UAAM,IAAI,OAAO,UAAU,EAAE,UAAU,OAAO,GAAG,WAAW,GAAG,MAAM,CAAC;AACtE,cAAU,CAAC;AACX,WAAO,MAAM;AACX,UAAI;AACF,UAAE,QAAQ;AAAA,MACZ,QAAQ;AAAA,MAGR;AAAA,IACF;AAAA,EACF,GAAG,CAAC,QAAQ,KAAK,CAAC;AAElB,SAAO;AACT;","names":[]}
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var use_pane_input_exports = {};
20
+ __export(use_pane_input_exports, {
21
+ usePaneInput: () => usePaneInput
22
+ });
23
+ module.exports = __toCommonJS(use_pane_input_exports);
24
+ var import_react = require("react");
25
+ function usePaneInput(parent, key, initialValue, options = {}) {
26
+ const [value, setValueState] = (0, import_react.useState)(initialValue);
27
+ const paramsRef = (0, import_react.useRef)({ [key]: initialValue });
28
+ const bindingRef = (0, import_react.useRef)(null);
29
+ const getOptions = (0, import_react.useEffectEvent)(() => options);
30
+ const getValue = (0, import_react.useEffectEvent)(() => value);
31
+ (0, import_react.useLayoutEffect)(() => {
32
+ if (!parent) {
33
+ bindingRef.current = null;
34
+ return;
35
+ }
36
+ paramsRef.current[key] = getValue();
37
+ const { label, ...bindingOpts } = getOptions();
38
+ const binding = parent.addBinding(paramsRef.current, key, {
39
+ label: label ?? key,
40
+ ...bindingOpts
41
+ });
42
+ binding.on("change", (ev) => {
43
+ setValueState(ev.value);
44
+ });
45
+ bindingRef.current = binding;
46
+ return () => {
47
+ try {
48
+ binding.dispose();
49
+ } catch {
50
+ }
51
+ if (bindingRef.current === binding) {
52
+ bindingRef.current = null;
53
+ }
54
+ };
55
+ }, [parent, key]);
56
+ const setValue = (0, import_react.useCallback)(
57
+ (v) => {
58
+ paramsRef.current[key] = v;
59
+ setValueState(v);
60
+ bindingRef.current?.refresh();
61
+ },
62
+ [key]
63
+ );
64
+ return [value, setValue];
65
+ }
66
+ // Annotate the CommonJS export names for ESM import in node:
67
+ 0 && (module.exports = {
68
+ usePaneInput
69
+ });
70
+ //# sourceMappingURL=use-pane-input.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/use-pane-input.ts"],"sourcesContent":["import { useCallback, useEffectEvent, useLayoutEffect, useRef, useState } from 'react'\nimport type { FolderApi, Pane } from 'tweakpane'\n\nexport type PaneParent = Pane | FolderApi\n\nexport interface PaneInputOptions {\n /** Minimum value (for number inputs, creates a slider) */\n min?: number\n /** Maximum value (for number inputs) */\n max?: number\n /** Step increment */\n step?: number\n /** Named options for list/dropdown binding */\n options?: Record<string, unknown>\n /** Label override (defaults to key) */\n label?: string\n /** Display as color picker */\n color?: { type: 'float' }\n /** Inverted range */\n inverted?: boolean\n /**\n * Render as a read-only monitor (no interactive editor). Value still\n * updates when `setValue` is called; tweakpane repaints on refresh.\n */\n readonly?: boolean\n /**\n * Custom display formatter (e.g. `(v) => v.toFixed(2)`). Forwarded to\n * tweakpane's native `format` option.\n */\n format?: (value: number) => string\n}\n\n/**\n * Bind a Tweakpane input to React state.\n * Returns [value, setValue] — setValue updates both React state and the\n * Tweakpane binding.\n *\n * The binding is created in `useLayoutEffect` and rebuilt whenever\n * `parent` or `key` changes — so when `usePane` tears down and rebuilds\n * its bundle (StrictMode, true remount), this hook automatically\n * re-binds to the new pane. Cleanup disposes the binding immediately.\n */\nexport function usePaneInput<T>(\n parent: PaneParent | null,\n key: string,\n initialValue: T,\n options: PaneInputOptions = {},\n): [T, (v: T) => void] {\n const [value, setValueState] = useState<T>(initialValue)\n const paramsRef = useRef<Record<string, unknown>>({ [key]: initialValue })\n const bindingRef = useRef<{ refresh(): void; dispose(): void } | null>(null)\n\n // Capture the latest options + value non-reactively, read only when the\n // binding is (re)created. useEffectEvent avoids touching refs during\n // render while keeping options/value out of the effect's dep list (so\n // they don't retrigger setup). tweakpane writes the bound object back on\n // its own change events; setValue keeps it synced for external updates.\n const getOptions = useEffectEvent(() => options)\n const getValue = useEffectEvent(() => value)\n\n useLayoutEffect(() => {\n if (!parent) {\n bindingRef.current = null\n return\n }\n\n // Seed the bound object with the latest value so a recreated binding\n // shows current state, not the initial.\n paramsRef.current[key] = getValue()\n const { label, ...bindingOpts } = getOptions()\n const binding = parent.addBinding(paramsRef.current, key, {\n label: label ?? key,\n ...bindingOpts,\n } as Record<string, unknown>)\n\n binding.on('change', (ev: { value: unknown }) => {\n setValueState(ev.value as T)\n })\n bindingRef.current = binding as unknown as { refresh(): void; dispose(): void }\n\n return () => {\n try {\n binding.dispose()\n } catch {\n // Parent may have been disposed first (cascade). Ignore.\n }\n if (\n bindingRef.current ===\n (binding as unknown as { refresh(): void; dispose(): void })\n ) {\n bindingRef.current = null\n }\n }\n // options/value are read via useEffectEvent, so they stay out of the\n // dep list and don't retrigger setup — only parent/key rebind.\n }, [parent, key])\n\n const setValue = useCallback(\n (v: T) => {\n paramsRef.current[key] = v\n setValueState(v)\n bindingRef.current?.refresh()\n },\n [key],\n )\n\n return [value, setValue]\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA+E;AA0CxE,SAAS,aACd,QACA,KACA,cACA,UAA4B,CAAC,GACR;AACrB,QAAM,CAAC,OAAO,aAAa,QAAI,uBAAY,YAAY;AACvD,QAAM,gBAAY,qBAAgC,EAAE,CAAC,GAAG,GAAG,aAAa,CAAC;AACzE,QAAM,iBAAa,qBAAoD,IAAI;AAO3E,QAAM,iBAAa,6BAAe,MAAM,OAAO;AAC/C,QAAM,eAAW,6BAAe,MAAM,KAAK;AAE3C,oCAAgB,MAAM;AACpB,QAAI,CAAC,QAAQ;AACX,iBAAW,UAAU;AACrB;AAAA,IACF;AAIA,cAAU,QAAQ,GAAG,IAAI,SAAS;AAClC,UAAM,EAAE,OAAO,GAAG,YAAY,IAAI,WAAW;AAC7C,UAAM,UAAU,OAAO,WAAW,UAAU,SAAS,KAAK;AAAA,MACxD,OAAO,SAAS;AAAA,MAChB,GAAG;AAAA,IACL,CAA4B;AAE5B,YAAQ,GAAG,UAAU,CAAC,OAA2B;AAC/C,oBAAc,GAAG,KAAU;AAAA,IAC7B,CAAC;AACD,eAAW,UAAU;AAErB,WAAO,MAAM;AACX,UAAI;AACF,gBAAQ,QAAQ;AAAA,MAClB,QAAQ;AAAA,MAER;AACA,UACE,WAAW,YACV,SACD;AACA,mBAAW,UAAU;AAAA,MACvB;AAAA,IACF;AAAA,EAGF,GAAG,CAAC,QAAQ,GAAG,CAAC;AAEhB,QAAM,eAAW;AAAA,IACf,CAAC,MAAS;AACR,gBAAU,QAAQ,GAAG,IAAI;AACzB,oBAAc,CAAC;AACf,iBAAW,SAAS,QAAQ;AAAA,IAC9B;AAAA,IACA,CAAC,GAAG;AAAA,EACN;AAEA,SAAO,CAAC,OAAO,QAAQ;AACzB;","names":[]}
@@ -0,0 +1,44 @@
1
+ import { Pane, FolderApi } from 'tweakpane';
2
+
3
+ type PaneParent = Pane | FolderApi;
4
+ interface PaneInputOptions {
5
+ /** Minimum value (for number inputs, creates a slider) */
6
+ min?: number;
7
+ /** Maximum value (for number inputs) */
8
+ max?: number;
9
+ /** Step increment */
10
+ step?: number;
11
+ /** Named options for list/dropdown binding */
12
+ options?: Record<string, unknown>;
13
+ /** Label override (defaults to key) */
14
+ label?: string;
15
+ /** Display as color picker */
16
+ color?: {
17
+ type: 'float';
18
+ };
19
+ /** Inverted range */
20
+ inverted?: boolean;
21
+ /**
22
+ * Render as a read-only monitor (no interactive editor). Value still
23
+ * updates when `setValue` is called; tweakpane repaints on refresh.
24
+ */
25
+ readonly?: boolean;
26
+ /**
27
+ * Custom display formatter (e.g. `(v) => v.toFixed(2)`). Forwarded to
28
+ * tweakpane's native `format` option.
29
+ */
30
+ format?: (value: number) => string;
31
+ }
32
+ /**
33
+ * Bind a Tweakpane input to React state.
34
+ * Returns [value, setValue] — setValue updates both React state and the
35
+ * Tweakpane binding.
36
+ *
37
+ * The binding is created in `useLayoutEffect` and rebuilt whenever
38
+ * `parent` or `key` changes — so when `usePane` tears down and rebuilds
39
+ * its bundle (StrictMode, true remount), this hook automatically
40
+ * re-binds to the new pane. Cleanup disposes the binding immediately.
41
+ */
42
+ declare function usePaneInput<T>(parent: PaneParent | null, key: string, initialValue: T, options?: PaneInputOptions): [T, (v: T) => void];
43
+
44
+ export { type PaneInputOptions, type PaneParent, usePaneInput };
@@ -0,0 +1,44 @@
1
+ import { Pane, FolderApi } from 'tweakpane';
2
+
3
+ type PaneParent = Pane | FolderApi;
4
+ interface PaneInputOptions {
5
+ /** Minimum value (for number inputs, creates a slider) */
6
+ min?: number;
7
+ /** Maximum value (for number inputs) */
8
+ max?: number;
9
+ /** Step increment */
10
+ step?: number;
11
+ /** Named options for list/dropdown binding */
12
+ options?: Record<string, unknown>;
13
+ /** Label override (defaults to key) */
14
+ label?: string;
15
+ /** Display as color picker */
16
+ color?: {
17
+ type: 'float';
18
+ };
19
+ /** Inverted range */
20
+ inverted?: boolean;
21
+ /**
22
+ * Render as a read-only monitor (no interactive editor). Value still
23
+ * updates when `setValue` is called; tweakpane repaints on refresh.
24
+ */
25
+ readonly?: boolean;
26
+ /**
27
+ * Custom display formatter (e.g. `(v) => v.toFixed(2)`). Forwarded to
28
+ * tweakpane's native `format` option.
29
+ */
30
+ format?: (value: number) => string;
31
+ }
32
+ /**
33
+ * Bind a Tweakpane input to React state.
34
+ * Returns [value, setValue] — setValue updates both React state and the
35
+ * Tweakpane binding.
36
+ *
37
+ * The binding is created in `useLayoutEffect` and rebuilt whenever
38
+ * `parent` or `key` changes — so when `usePane` tears down and rebuilds
39
+ * its bundle (StrictMode, true remount), this hook automatically
40
+ * re-binds to the new pane. Cleanup disposes the binding immediately.
41
+ */
42
+ declare function usePaneInput<T>(parent: PaneParent | null, key: string, initialValue: T, options?: PaneInputOptions): [T, (v: T) => void];
43
+
44
+ export { type PaneInputOptions, type PaneParent, usePaneInput };
@@ -0,0 +1,46 @@
1
+ import { useCallback, useEffectEvent, useLayoutEffect, useRef, useState } from "react";
2
+ function usePaneInput(parent, key, initialValue, options = {}) {
3
+ const [value, setValueState] = useState(initialValue);
4
+ const paramsRef = useRef({ [key]: initialValue });
5
+ const bindingRef = useRef(null);
6
+ const getOptions = useEffectEvent(() => options);
7
+ const getValue = useEffectEvent(() => value);
8
+ useLayoutEffect(() => {
9
+ if (!parent) {
10
+ bindingRef.current = null;
11
+ return;
12
+ }
13
+ paramsRef.current[key] = getValue();
14
+ const { label, ...bindingOpts } = getOptions();
15
+ const binding = parent.addBinding(paramsRef.current, key, {
16
+ label: label ?? key,
17
+ ...bindingOpts
18
+ });
19
+ binding.on("change", (ev) => {
20
+ setValueState(ev.value);
21
+ });
22
+ bindingRef.current = binding;
23
+ return () => {
24
+ try {
25
+ binding.dispose();
26
+ } catch {
27
+ }
28
+ if (bindingRef.current === binding) {
29
+ bindingRef.current = null;
30
+ }
31
+ };
32
+ }, [parent, key]);
33
+ const setValue = useCallback(
34
+ (v) => {
35
+ paramsRef.current[key] = v;
36
+ setValueState(v);
37
+ bindingRef.current?.refresh();
38
+ },
39
+ [key]
40
+ );
41
+ return [value, setValue];
42
+ }
43
+ export {
44
+ usePaneInput
45
+ };
46
+ //# sourceMappingURL=use-pane-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/use-pane-input.ts"],"sourcesContent":["import { useCallback, useEffectEvent, useLayoutEffect, useRef, useState } from 'react'\nimport type { FolderApi, Pane } from 'tweakpane'\n\nexport type PaneParent = Pane | FolderApi\n\nexport interface PaneInputOptions {\n /** Minimum value (for number inputs, creates a slider) */\n min?: number\n /** Maximum value (for number inputs) */\n max?: number\n /** Step increment */\n step?: number\n /** Named options for list/dropdown binding */\n options?: Record<string, unknown>\n /** Label override (defaults to key) */\n label?: string\n /** Display as color picker */\n color?: { type: 'float' }\n /** Inverted range */\n inverted?: boolean\n /**\n * Render as a read-only monitor (no interactive editor). Value still\n * updates when `setValue` is called; tweakpane repaints on refresh.\n */\n readonly?: boolean\n /**\n * Custom display formatter (e.g. `(v) => v.toFixed(2)`). Forwarded to\n * tweakpane's native `format` option.\n */\n format?: (value: number) => string\n}\n\n/**\n * Bind a Tweakpane input to React state.\n * Returns [value, setValue] — setValue updates both React state and the\n * Tweakpane binding.\n *\n * The binding is created in `useLayoutEffect` and rebuilt whenever\n * `parent` or `key` changes — so when `usePane` tears down and rebuilds\n * its bundle (StrictMode, true remount), this hook automatically\n * re-binds to the new pane. Cleanup disposes the binding immediately.\n */\nexport function usePaneInput<T>(\n parent: PaneParent | null,\n key: string,\n initialValue: T,\n options: PaneInputOptions = {},\n): [T, (v: T) => void] {\n const [value, setValueState] = useState<T>(initialValue)\n const paramsRef = useRef<Record<string, unknown>>({ [key]: initialValue })\n const bindingRef = useRef<{ refresh(): void; dispose(): void } | null>(null)\n\n // Capture the latest options + value non-reactively, read only when the\n // binding is (re)created. useEffectEvent avoids touching refs during\n // render while keeping options/value out of the effect's dep list (so\n // they don't retrigger setup). tweakpane writes the bound object back on\n // its own change events; setValue keeps it synced for external updates.\n const getOptions = useEffectEvent(() => options)\n const getValue = useEffectEvent(() => value)\n\n useLayoutEffect(() => {\n if (!parent) {\n bindingRef.current = null\n return\n }\n\n // Seed the bound object with the latest value so a recreated binding\n // shows current state, not the initial.\n paramsRef.current[key] = getValue()\n const { label, ...bindingOpts } = getOptions()\n const binding = parent.addBinding(paramsRef.current, key, {\n label: label ?? key,\n ...bindingOpts,\n } as Record<string, unknown>)\n\n binding.on('change', (ev: { value: unknown }) => {\n setValueState(ev.value as T)\n })\n bindingRef.current = binding as unknown as { refresh(): void; dispose(): void }\n\n return () => {\n try {\n binding.dispose()\n } catch {\n // Parent may have been disposed first (cascade). Ignore.\n }\n if (\n bindingRef.current ===\n (binding as unknown as { refresh(): void; dispose(): void })\n ) {\n bindingRef.current = null\n }\n }\n // options/value are read via useEffectEvent, so they stay out of the\n // dep list and don't retrigger setup — only parent/key rebind.\n }, [parent, key])\n\n const setValue = useCallback(\n (v: T) => {\n paramsRef.current[key] = v\n setValueState(v)\n bindingRef.current?.refresh()\n },\n [key],\n )\n\n return [value, setValue]\n}\n"],"mappings":"AAAA,SAAS,aAAa,gBAAgB,iBAAiB,QAAQ,gBAAgB;AA0CxE,SAAS,aACd,QACA,KACA,cACA,UAA4B,CAAC,GACR;AACrB,QAAM,CAAC,OAAO,aAAa,IAAI,SAAY,YAAY;AACvD,QAAM,YAAY,OAAgC,EAAE,CAAC,GAAG,GAAG,aAAa,CAAC;AACzE,QAAM,aAAa,OAAoD,IAAI;AAO3E,QAAM,aAAa,eAAe,MAAM,OAAO;AAC/C,QAAM,WAAW,eAAe,MAAM,KAAK;AAE3C,kBAAgB,MAAM;AACpB,QAAI,CAAC,QAAQ;AACX,iBAAW,UAAU;AACrB;AAAA,IACF;AAIA,cAAU,QAAQ,GAAG,IAAI,SAAS;AAClC,UAAM,EAAE,OAAO,GAAG,YAAY,IAAI,WAAW;AAC7C,UAAM,UAAU,OAAO,WAAW,UAAU,SAAS,KAAK;AAAA,MACxD,OAAO,SAAS;AAAA,MAChB,GAAG;AAAA,IACL,CAA4B;AAE5B,YAAQ,GAAG,UAAU,CAAC,OAA2B;AAC/C,oBAAc,GAAG,KAAU;AAAA,IAC7B,CAAC;AACD,eAAW,UAAU;AAErB,WAAO,MAAM;AACX,UAAI;AACF,gBAAQ,QAAQ;AAAA,MAClB,QAAQ;AAAA,MAER;AACA,UACE,WAAW,YACV,SACD;AACA,mBAAW,UAAU;AAAA,MACvB;AAAA,IACF;AAAA,EAGF,GAAG,CAAC,QAAQ,GAAG,CAAC;AAEhB,QAAM,WAAW;AAAA,IACf,CAAC,MAAS;AACR,gBAAU,QAAQ,GAAG,IAAI;AACzB,oBAAc,CAAC;AACf,iBAAW,SAAS,QAAQ;AAAA,IAC9B;AAAA,IACA,CAAC,GAAG;AAAA,EACN;AAEA,SAAO,CAAC,OAAO,QAAQ;AACzB;","names":[]}
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var use_pane_radio_grid_exports = {};
20
+ __export(use_pane_radio_grid_exports, {
21
+ usePaneRadioGrid: () => usePaneRadioGrid
22
+ });
23
+ module.exports = __toCommonJS(use_pane_radio_grid_exports);
24
+ var import_react = require("react");
25
+ function usePaneRadioGrid(parent, options) {
26
+ const { cells, initialValue, groupName, size } = options;
27
+ const [value, setValueState] = (0, import_react.useState)(initialValue);
28
+ const bladeRef = (0, import_react.useRef)(null);
29
+ const cellsRef = (0, import_react.useRef)(cells);
30
+ cellsRef.current = cells;
31
+ const mountedRef = (0, import_react.useRef)(false);
32
+ const ensureBlade = (0, import_react.useCallback)(() => {
33
+ if (!parent || bladeRef.current) return;
34
+ const [cols, rows] = size ?? [cells.length, 1];
35
+ const bag = { value: initialValue };
36
+ const blade = parent.addBlade({
37
+ view: "radiogrid",
38
+ groupName: groupName ?? `radio-${Math.random().toString(36).slice(2, 10)}`,
39
+ size: [cols, rows],
40
+ cells: (x, y) => {
41
+ const idx = y * cols + x;
42
+ const cell = cellsRef.current[idx];
43
+ return cell ? { title: cell.title, value: cell.value } : { title: "", value: void 0 };
44
+ },
45
+ value: bag.value
46
+ });
47
+ blade?.on("change", (ev) => {
48
+ setValueState(ev.value);
49
+ });
50
+ bladeRef.current = blade;
51
+ }, [parent, groupName, initialValue, cells.length, size]);
52
+ if (parent && bladeRef.current === null) ensureBlade();
53
+ (0, import_react.useEffect)(() => {
54
+ mountedRef.current = true;
55
+ if (parent && bladeRef.current === null) ensureBlade();
56
+ return () => {
57
+ const blade = bladeRef.current;
58
+ setTimeout(() => {
59
+ if (!mountedRef.current && blade) {
60
+ blade.dispose();
61
+ if (bladeRef.current === blade) bladeRef.current = null;
62
+ }
63
+ }, 0);
64
+ mountedRef.current = false;
65
+ };
66
+ }, [parent]);
67
+ const setValue = (0, import_react.useCallback)((v) => {
68
+ setValueState(v);
69
+ const blade = bladeRef.current;
70
+ if (blade && "value" in blade) blade.value = v;
71
+ }, []);
72
+ return [value, setValue];
73
+ }
74
+ // Annotate the CommonJS export names for ESM import in node:
75
+ 0 && (module.exports = {
76
+ usePaneRadioGrid
77
+ });
78
+ //# sourceMappingURL=use-pane-radio-grid.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/use-pane-radio-grid.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport type { PaneParent } from './use-pane-input.js'\n\nexport interface PaneRadioGridCell<T> {\n /** Visible label on the button. */\n title: string\n /** Value emitted by this cell when selected. */\n value: T\n}\n\nexport interface PaneRadioGridOptions<T> {\n /** Radio-group name. Required by Tweakpane — any unique string works. */\n groupName?: string\n /** Initially-selected value. Must match one of `cells[i].value`. */\n initialValue: T\n /**\n * Grid cells in row-major order. Default layout is a single row with\n * `cells.length` columns, matching a top-of-pane toggle bar.\n */\n cells: ReadonlyArray<PaneRadioGridCell<T>>\n /** Explicit `[cols, rows]`. Defaults to `[cells.length, 1]`. */\n size?: [number, number]\n}\n\n/**\n * Render an inline button-bar selector backed by the Tweakpane Essentials\n * `radiogrid` blade. Returns `[value, setValue]` — interacting with the\n * pane buttons updates React state, and `setValue` pushes back into the\n * blade. Intended for scene/mode toggles where a dropdown feels too\n * heavyweight and a row of labeled buttons reads better.\n *\n * Disposal is deferred through `setTimeout(0)` so the blade survives\n * React strict-mode's synchronous cleanup/re-mount pair.\n */\nexport function usePaneRadioGrid<T>(\n parent: PaneParent | null,\n options: PaneRadioGridOptions<T>,\n): [T, (v: T) => void] {\n const { cells, initialValue, groupName, size } = options\n const [value, setValueState] = useState<T>(initialValue)\n\n const bladeRef = useRef<{ dispose(): void; on: (ev: string, fn: (e: { value: T }) => void) => unknown; value?: T } | null>(null)\n const cellsRef = useRef(cells)\n cellsRef.current = cells\n const mountedRef = useRef(false)\n\n const ensureBlade = useCallback(() => {\n if (!parent || bladeRef.current) return\n\n const [cols, rows] = size ?? [cells.length, 1]\n const bag = { value: initialValue }\n const blade = (parent as unknown as {\n addBlade: (opts: Record<string, unknown>) => typeof bladeRef.current\n }).addBlade({\n view: 'radiogrid',\n groupName: groupName ?? `radio-${Math.random().toString(36).slice(2, 10)}`,\n size: [cols, rows],\n cells: (x: number, y: number) => {\n const idx = y * cols + x\n const cell = cellsRef.current[idx]\n return cell ? { title: cell.title, value: cell.value } : { title: '', value: undefined }\n },\n value: bag.value,\n })\n\n blade?.on('change', (ev) => {\n setValueState(ev.value)\n })\n bladeRef.current = blade\n }, [parent, groupName, initialValue, cells.length, size])\n\n // Synchronous creation so the toggle bar appears on first paint.\n if (parent && bladeRef.current === null) ensureBlade()\n\n useEffect(() => {\n mountedRef.current = true\n if (parent && bladeRef.current === null) ensureBlade()\n\n return () => {\n const blade = bladeRef.current\n setTimeout(() => {\n if (!mountedRef.current && blade) {\n blade.dispose()\n if (bladeRef.current === blade) bladeRef.current = null\n }\n }, 0)\n mountedRef.current = false\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [parent])\n\n const setValue = useCallback((v: T) => {\n setValueState(v)\n const blade = bladeRef.current as (typeof bladeRef.current & { value?: T }) | null\n if (blade && 'value' in blade) blade.value = v\n }, [])\n\n return [value, setValue]\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAyD;AAkClD,SAAS,iBACd,QACA,SACqB;AACrB,QAAM,EAAE,OAAO,cAAc,WAAW,KAAK,IAAI;AACjD,QAAM,CAAC,OAAO,aAAa,QAAI,uBAAY,YAAY;AAEvD,QAAM,eAAW,qBAA0G,IAAI;AAC/H,QAAM,eAAW,qBAAO,KAAK;AAC7B,WAAS,UAAU;AACnB,QAAM,iBAAa,qBAAO,KAAK;AAE/B,QAAM,kBAAc,0BAAY,MAAM;AACpC,QAAI,CAAC,UAAU,SAAS,QAAS;AAEjC,UAAM,CAAC,MAAM,IAAI,IAAI,QAAQ,CAAC,MAAM,QAAQ,CAAC;AAC7C,UAAM,MAAM,EAAE,OAAO,aAAa;AAClC,UAAM,QAAS,OAEZ,SAAS;AAAA,MACV,MAAM;AAAA,MACN,WAAW,aAAa,SAAS,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AAAA,MACxE,MAAM,CAAC,MAAM,IAAI;AAAA,MACjB,OAAO,CAAC,GAAW,MAAc;AAC/B,cAAM,MAAM,IAAI,OAAO;AACvB,cAAM,OAAO,SAAS,QAAQ,GAAG;AACjC,eAAO,OAAO,EAAE,OAAO,KAAK,OAAO,OAAO,KAAK,MAAM,IAAI,EAAE,OAAO,IAAI,OAAO,OAAU;AAAA,MACzF;AAAA,MACA,OAAO,IAAI;AAAA,IACb,CAAC;AAED,WAAO,GAAG,UAAU,CAAC,OAAO;AAC1B,oBAAc,GAAG,KAAK;AAAA,IACxB,CAAC;AACD,aAAS,UAAU;AAAA,EACrB,GAAG,CAAC,QAAQ,WAAW,cAAc,MAAM,QAAQ,IAAI,CAAC;AAGxD,MAAI,UAAU,SAAS,YAAY,KAAM,aAAY;AAErD,8BAAU,MAAM;AACd,eAAW,UAAU;AACrB,QAAI,UAAU,SAAS,YAAY,KAAM,aAAY;AAErD,WAAO,MAAM;AACX,YAAM,QAAQ,SAAS;AACvB,iBAAW,MAAM;AACf,YAAI,CAAC,WAAW,WAAW,OAAO;AAChC,gBAAM,QAAQ;AACd,cAAI,SAAS,YAAY,MAAO,UAAS,UAAU;AAAA,QACrD;AAAA,MACF,GAAG,CAAC;AACJ,iBAAW,UAAU;AAAA,IACvB;AAAA,EAEF,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,eAAW,0BAAY,CAAC,MAAS;AACrC,kBAAc,CAAC;AACf,UAAM,QAAQ,SAAS;AACvB,QAAI,SAAS,WAAW,MAAO,OAAM,QAAQ;AAAA,EAC/C,GAAG,CAAC,CAAC;AAEL,SAAO,CAAC,OAAO,QAAQ;AACzB;","names":[]}
@@ -0,0 +1,35 @@
1
+ import { PaneParent } from './use-pane-input.cjs';
2
+ import 'tweakpane';
3
+
4
+ interface PaneRadioGridCell<T> {
5
+ /** Visible label on the button. */
6
+ title: string;
7
+ /** Value emitted by this cell when selected. */
8
+ value: T;
9
+ }
10
+ interface PaneRadioGridOptions<T> {
11
+ /** Radio-group name. Required by Tweakpane — any unique string works. */
12
+ groupName?: string;
13
+ /** Initially-selected value. Must match one of `cells[i].value`. */
14
+ initialValue: T;
15
+ /**
16
+ * Grid cells in row-major order. Default layout is a single row with
17
+ * `cells.length` columns, matching a top-of-pane toggle bar.
18
+ */
19
+ cells: ReadonlyArray<PaneRadioGridCell<T>>;
20
+ /** Explicit `[cols, rows]`. Defaults to `[cells.length, 1]`. */
21
+ size?: [number, number];
22
+ }
23
+ /**
24
+ * Render an inline button-bar selector backed by the Tweakpane Essentials
25
+ * `radiogrid` blade. Returns `[value, setValue]` — interacting with the
26
+ * pane buttons updates React state, and `setValue` pushes back into the
27
+ * blade. Intended for scene/mode toggles where a dropdown feels too
28
+ * heavyweight and a row of labeled buttons reads better.
29
+ *
30
+ * Disposal is deferred through `setTimeout(0)` so the blade survives
31
+ * React strict-mode's synchronous cleanup/re-mount pair.
32
+ */
33
+ declare function usePaneRadioGrid<T>(parent: PaneParent | null, options: PaneRadioGridOptions<T>): [T, (v: T) => void];
34
+
35
+ export { type PaneRadioGridCell, type PaneRadioGridOptions, usePaneRadioGrid };
@@ -0,0 +1,35 @@
1
+ import { PaneParent } from './use-pane-input.js';
2
+ import 'tweakpane';
3
+
4
+ interface PaneRadioGridCell<T> {
5
+ /** Visible label on the button. */
6
+ title: string;
7
+ /** Value emitted by this cell when selected. */
8
+ value: T;
9
+ }
10
+ interface PaneRadioGridOptions<T> {
11
+ /** Radio-group name. Required by Tweakpane — any unique string works. */
12
+ groupName?: string;
13
+ /** Initially-selected value. Must match one of `cells[i].value`. */
14
+ initialValue: T;
15
+ /**
16
+ * Grid cells in row-major order. Default layout is a single row with
17
+ * `cells.length` columns, matching a top-of-pane toggle bar.
18
+ */
19
+ cells: ReadonlyArray<PaneRadioGridCell<T>>;
20
+ /** Explicit `[cols, rows]`. Defaults to `[cells.length, 1]`. */
21
+ size?: [number, number];
22
+ }
23
+ /**
24
+ * Render an inline button-bar selector backed by the Tweakpane Essentials
25
+ * `radiogrid` blade. Returns `[value, setValue]` — interacting with the
26
+ * pane buttons updates React state, and `setValue` pushes back into the
27
+ * blade. Intended for scene/mode toggles where a dropdown feels too
28
+ * heavyweight and a row of labeled buttons reads better.
29
+ *
30
+ * Disposal is deferred through `setTimeout(0)` so the blade survives
31
+ * React strict-mode's synchronous cleanup/re-mount pair.
32
+ */
33
+ declare function usePaneRadioGrid<T>(parent: PaneParent | null, options: PaneRadioGridOptions<T>): [T, (v: T) => void];
34
+
35
+ export { type PaneRadioGridCell, type PaneRadioGridOptions, usePaneRadioGrid };
@@ -0,0 +1,54 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ function usePaneRadioGrid(parent, options) {
3
+ const { cells, initialValue, groupName, size } = options;
4
+ const [value, setValueState] = useState(initialValue);
5
+ const bladeRef = useRef(null);
6
+ const cellsRef = useRef(cells);
7
+ cellsRef.current = cells;
8
+ const mountedRef = useRef(false);
9
+ const ensureBlade = useCallback(() => {
10
+ if (!parent || bladeRef.current) return;
11
+ const [cols, rows] = size ?? [cells.length, 1];
12
+ const bag = { value: initialValue };
13
+ const blade = parent.addBlade({
14
+ view: "radiogrid",
15
+ groupName: groupName ?? `radio-${Math.random().toString(36).slice(2, 10)}`,
16
+ size: [cols, rows],
17
+ cells: (x, y) => {
18
+ const idx = y * cols + x;
19
+ const cell = cellsRef.current[idx];
20
+ return cell ? { title: cell.title, value: cell.value } : { title: "", value: void 0 };
21
+ },
22
+ value: bag.value
23
+ });
24
+ blade?.on("change", (ev) => {
25
+ setValueState(ev.value);
26
+ });
27
+ bladeRef.current = blade;
28
+ }, [parent, groupName, initialValue, cells.length, size]);
29
+ if (parent && bladeRef.current === null) ensureBlade();
30
+ useEffect(() => {
31
+ mountedRef.current = true;
32
+ if (parent && bladeRef.current === null) ensureBlade();
33
+ return () => {
34
+ const blade = bladeRef.current;
35
+ setTimeout(() => {
36
+ if (!mountedRef.current && blade) {
37
+ blade.dispose();
38
+ if (bladeRef.current === blade) bladeRef.current = null;
39
+ }
40
+ }, 0);
41
+ mountedRef.current = false;
42
+ };
43
+ }, [parent]);
44
+ const setValue = useCallback((v) => {
45
+ setValueState(v);
46
+ const blade = bladeRef.current;
47
+ if (blade && "value" in blade) blade.value = v;
48
+ }, []);
49
+ return [value, setValue];
50
+ }
51
+ export {
52
+ usePaneRadioGrid
53
+ };
54
+ //# sourceMappingURL=use-pane-radio-grid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/use-pane-radio-grid.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport type { PaneParent } from './use-pane-input.js'\n\nexport interface PaneRadioGridCell<T> {\n /** Visible label on the button. */\n title: string\n /** Value emitted by this cell when selected. */\n value: T\n}\n\nexport interface PaneRadioGridOptions<T> {\n /** Radio-group name. Required by Tweakpane — any unique string works. */\n groupName?: string\n /** Initially-selected value. Must match one of `cells[i].value`. */\n initialValue: T\n /**\n * Grid cells in row-major order. Default layout is a single row with\n * `cells.length` columns, matching a top-of-pane toggle bar.\n */\n cells: ReadonlyArray<PaneRadioGridCell<T>>\n /** Explicit `[cols, rows]`. Defaults to `[cells.length, 1]`. */\n size?: [number, number]\n}\n\n/**\n * Render an inline button-bar selector backed by the Tweakpane Essentials\n * `radiogrid` blade. Returns `[value, setValue]` — interacting with the\n * pane buttons updates React state, and `setValue` pushes back into the\n * blade. Intended for scene/mode toggles where a dropdown feels too\n * heavyweight and a row of labeled buttons reads better.\n *\n * Disposal is deferred through `setTimeout(0)` so the blade survives\n * React strict-mode's synchronous cleanup/re-mount pair.\n */\nexport function usePaneRadioGrid<T>(\n parent: PaneParent | null,\n options: PaneRadioGridOptions<T>,\n): [T, (v: T) => void] {\n const { cells, initialValue, groupName, size } = options\n const [value, setValueState] = useState<T>(initialValue)\n\n const bladeRef = useRef<{ dispose(): void; on: (ev: string, fn: (e: { value: T }) => void) => unknown; value?: T } | null>(null)\n const cellsRef = useRef(cells)\n cellsRef.current = cells\n const mountedRef = useRef(false)\n\n const ensureBlade = useCallback(() => {\n if (!parent || bladeRef.current) return\n\n const [cols, rows] = size ?? [cells.length, 1]\n const bag = { value: initialValue }\n const blade = (parent as unknown as {\n addBlade: (opts: Record<string, unknown>) => typeof bladeRef.current\n }).addBlade({\n view: 'radiogrid',\n groupName: groupName ?? `radio-${Math.random().toString(36).slice(2, 10)}`,\n size: [cols, rows],\n cells: (x: number, y: number) => {\n const idx = y * cols + x\n const cell = cellsRef.current[idx]\n return cell ? { title: cell.title, value: cell.value } : { title: '', value: undefined }\n },\n value: bag.value,\n })\n\n blade?.on('change', (ev) => {\n setValueState(ev.value)\n })\n bladeRef.current = blade\n }, [parent, groupName, initialValue, cells.length, size])\n\n // Synchronous creation so the toggle bar appears on first paint.\n if (parent && bladeRef.current === null) ensureBlade()\n\n useEffect(() => {\n mountedRef.current = true\n if (parent && bladeRef.current === null) ensureBlade()\n\n return () => {\n const blade = bladeRef.current\n setTimeout(() => {\n if (!mountedRef.current && blade) {\n blade.dispose()\n if (bladeRef.current === blade) bladeRef.current = null\n }\n }, 0)\n mountedRef.current = false\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [parent])\n\n const setValue = useCallback((v: T) => {\n setValueState(v)\n const blade = bladeRef.current as (typeof bladeRef.current & { value?: T }) | null\n if (blade && 'value' in blade) blade.value = v\n }, [])\n\n return [value, setValue]\n}\n"],"mappings":"AAAA,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AAkClD,SAAS,iBACd,QACA,SACqB;AACrB,QAAM,EAAE,OAAO,cAAc,WAAW,KAAK,IAAI;AACjD,QAAM,CAAC,OAAO,aAAa,IAAI,SAAY,YAAY;AAEvD,QAAM,WAAW,OAA0G,IAAI;AAC/H,QAAM,WAAW,OAAO,KAAK;AAC7B,WAAS,UAAU;AACnB,QAAM,aAAa,OAAO,KAAK;AAE/B,QAAM,cAAc,YAAY,MAAM;AACpC,QAAI,CAAC,UAAU,SAAS,QAAS;AAEjC,UAAM,CAAC,MAAM,IAAI,IAAI,QAAQ,CAAC,MAAM,QAAQ,CAAC;AAC7C,UAAM,MAAM,EAAE,OAAO,aAAa;AAClC,UAAM,QAAS,OAEZ,SAAS;AAAA,MACV,MAAM;AAAA,MACN,WAAW,aAAa,SAAS,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AAAA,MACxE,MAAM,CAAC,MAAM,IAAI;AAAA,MACjB,OAAO,CAAC,GAAW,MAAc;AAC/B,cAAM,MAAM,IAAI,OAAO;AACvB,cAAM,OAAO,SAAS,QAAQ,GAAG;AACjC,eAAO,OAAO,EAAE,OAAO,KAAK,OAAO,OAAO,KAAK,MAAM,IAAI,EAAE,OAAO,IAAI,OAAO,OAAU;AAAA,MACzF;AAAA,MACA,OAAO,IAAI;AAAA,IACb,CAAC;AAED,WAAO,GAAG,UAAU,CAAC,OAAO;AAC1B,oBAAc,GAAG,KAAK;AAAA,IACxB,CAAC;AACD,aAAS,UAAU;AAAA,EACrB,GAAG,CAAC,QAAQ,WAAW,cAAc,MAAM,QAAQ,IAAI,CAAC;AAGxD,MAAI,UAAU,SAAS,YAAY,KAAM,aAAY;AAErD,YAAU,MAAM;AACd,eAAW,UAAU;AACrB,QAAI,UAAU,SAAS,YAAY,KAAM,aAAY;AAErD,WAAO,MAAM;AACX,YAAM,QAAQ,SAAS;AACvB,iBAAW,MAAM;AACf,YAAI,CAAC,WAAW,WAAW,OAAO;AAChC,gBAAM,QAAQ;AACd,cAAI,SAAS,YAAY,MAAO,UAAS,UAAU;AAAA,QACrD;AAAA,MACF,GAAG,CAAC;AACJ,iBAAW,UAAU;AAAA,IACvB;AAAA,EAEF,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,WAAW,YAAY,CAAC,MAAS;AACrC,kBAAc,CAAC;AACf,UAAM,QAAQ,SAAS;AACvB,QAAI,SAAS,WAAW,MAAO,OAAM,QAAQ;AAAA,EAC/C,GAAG,CAAC,CAAC;AAEL,SAAO,CAAC,OAAO,QAAQ;AACzB;","names":[]}