@three-flatland/devtools 0.1.0-alpha.3 → 1.0.0-alpha.6

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 (172) hide show
  1. package/README.md +1 -1
  2. package/dist/buffers-modal.d.ts +16 -52
  3. package/dist/buffers-modal.d.ts.map +1 -0
  4. package/dist/buffers-modal.js +459 -464
  5. package/dist/buffers-modal.js.map +1 -1
  6. package/dist/buffers-view.d.ts +22 -34
  7. package/dist/buffers-view.d.ts.map +1 -0
  8. package/dist/buffers-view.js +252 -240
  9. package/dist/buffers-view.js.map +1 -1
  10. package/dist/create-pane.d.ts +38 -37
  11. package/dist/create-pane.d.ts.map +1 -0
  12. package/dist/create-pane.js +197 -166
  13. package/dist/create-pane.js.map +1 -1
  14. package/dist/dashboard/app.tsx +2 -0
  15. package/dist/dashboard/export.ts +1 -3
  16. package/dist/dashboard/frame-cursor.ts +81 -0
  17. package/dist/dashboard/hooks.ts +10 -2
  18. package/dist/dashboard/index.html +61 -0
  19. package/dist/dashboard/panels/batches.tsx +5 -18
  20. package/dist/dashboard/panels/buffers.tsx +52 -26
  21. package/dist/dashboard/panels/env.tsx +5 -6
  22. package/dist/dashboard/panels/protocol-log.tsx +146 -58
  23. package/dist/dashboard/panels/registry.tsx +55 -69
  24. package/dist/dashboard/panels/scrubber.tsx +100 -0
  25. package/dist/dashboard/panels/stats.tsx +60 -32
  26. package/dist/dashboard/protocol-store.ts +25 -8
  27. package/dist/dashboard/vendor/hooks.module.d.ts +146 -0
  28. package/dist/dashboard/vendor/jsx-runtime.d.ts +55 -0
  29. package/dist/dashboard/vendor/jsx.d.ts +3123 -0
  30. package/dist/dashboard/vendor/preact.module.d.ts +397 -0
  31. package/dist/devtools-client.d.ts +312 -300
  32. package/dist/devtools-client.d.ts.map +1 -0
  33. package/dist/devtools-client.js +866 -838
  34. package/dist/devtools-client.js.map +1 -1
  35. package/dist/index.d.ts +6 -8
  36. package/dist/index.js +5 -12
  37. package/dist/perf-trace.d.ts +6 -4
  38. package/dist/perf-trace.d.ts.map +1 -0
  39. package/dist/perf-trace.js +80 -66
  40. package/dist/perf-trace.js.map +1 -1
  41. package/dist/plugins.d.ts +6 -6
  42. package/dist/plugins.d.ts.map +1 -0
  43. package/dist/plugins.js +5 -5
  44. package/dist/plugins.js.map +1 -1
  45. package/dist/provider-switcher.d.ts +8 -15
  46. package/dist/provider-switcher.d.ts.map +1 -0
  47. package/dist/provider-switcher.js +68 -68
  48. package/dist/provider-switcher.js.map +1 -1
  49. package/dist/react/devtools-provider.d.ts +11 -10
  50. package/dist/react/devtools-provider.d.ts.map +1 -0
  51. package/dist/react/devtools-provider.js +60 -32
  52. package/dist/react/devtools-provider.js.map +1 -1
  53. package/dist/react/use-fps-graph.d.ts +7 -7
  54. package/dist/react/use-fps-graph.d.ts.map +1 -0
  55. package/dist/react/use-fps-graph.js +27 -21
  56. package/dist/react/use-fps-graph.js.map +1 -1
  57. package/dist/react/use-pane-button.d.ts +4 -4
  58. package/dist/react/use-pane-button.d.ts.map +1 -0
  59. package/dist/react/use-pane-button.js +35 -30
  60. package/dist/react/use-pane-button.js.map +1 -1
  61. package/dist/react/use-pane-folder.d.ts +5 -4
  62. package/dist/react/use-pane-folder.d.ts.map +1 -0
  63. package/dist/react/use-pane-folder.js +33 -20
  64. package/dist/react/use-pane-folder.js.map +1 -1
  65. package/dist/react/use-pane-input.d.ts +31 -30
  66. package/dist/react/use-pane-input.d.ts.map +1 -0
  67. package/dist/react/use-pane-input.js +46 -42
  68. package/dist/react/use-pane-input.js.map +1 -1
  69. package/dist/react/use-pane-radio-grid.d.ts +20 -20
  70. package/dist/react/use-pane-radio-grid.d.ts.map +1 -0
  71. package/dist/react/use-pane-radio-grid.js +72 -50
  72. package/dist/react/use-pane-radio-grid.js.map +1 -1
  73. package/dist/react/use-pane.d.ts +4 -4
  74. package/dist/react/use-pane.d.ts.map +1 -0
  75. package/dist/react/use-pane.js +52 -17
  76. package/dist/react/use-pane.js.map +1 -1
  77. package/dist/react.d.ts +10 -12
  78. package/dist/react.js +2 -12
  79. package/dist/registry-view.d.ts +8 -21
  80. package/dist/registry-view.d.ts.map +1 -0
  81. package/dist/registry-view.js +357 -338
  82. package/dist/registry-view.js.map +1 -1
  83. package/dist/relay.d.ts +16 -0
  84. package/dist/relay.d.ts.map +1 -0
  85. package/dist/relay.js +189 -0
  86. package/dist/relay.js.map +1 -0
  87. package/dist/remote.d.ts +10 -0
  88. package/dist/remote.d.ts.map +1 -0
  89. package/dist/remote.js +28 -0
  90. package/dist/remote.js.map +1 -0
  91. package/dist/stats-graph.d.ts +23 -31
  92. package/dist/stats-graph.d.ts.map +1 -0
  93. package/dist/stats-graph.js +323 -264
  94. package/dist/stats-graph.js.map +1 -1
  95. package/dist/stats-row.d.ts +8 -21
  96. package/dist/stats-row.d.ts.map +1 -0
  97. package/dist/stats-row.js +99 -101
  98. package/dist/stats-row.js.map +1 -1
  99. package/dist/theme.d.ts +3 -1
  100. package/dist/theme.d.ts.map +1 -0
  101. package/dist/theme.js +43 -54
  102. package/dist/theme.js.map +1 -1
  103. package/dist/vite-plugin.d.ts +7 -6
  104. package/dist/vite-plugin.d.ts.map +1 -0
  105. package/dist/vite-plugin.js +87 -78
  106. package/dist/vite-plugin.js.map +1 -1
  107. package/package.json +33 -29
  108. package/dist/buffers-modal.cjs +0 -491
  109. package/dist/buffers-modal.cjs.map +0 -1
  110. package/dist/buffers-modal.d.cts +0 -62
  111. package/dist/buffers-view.cjs +0 -269
  112. package/dist/buffers-view.cjs.map +0 -1
  113. package/dist/buffers-view.d.cts +0 -39
  114. package/dist/create-pane.cjs +0 -203
  115. package/dist/create-pane.cjs.map +0 -1
  116. package/dist/create-pane.d.cts +0 -66
  117. package/dist/devtools-client.cjs +0 -870
  118. package/dist/devtools-client.cjs.map +0 -1
  119. package/dist/devtools-client.d.cts +0 -362
  120. package/dist/index.cjs +0 -42
  121. package/dist/index.cjs.map +0 -1
  122. package/dist/index.d.cts +0 -8
  123. package/dist/index.js.map +0 -1
  124. package/dist/perf-trace.cjs +0 -103
  125. package/dist/perf-trace.cjs.map +0 -1
  126. package/dist/perf-trace.d.cts +0 -27
  127. package/dist/plugins.cjs +0 -44
  128. package/dist/plugins.cjs.map +0 -1
  129. package/dist/plugins.d.cts +0 -7
  130. package/dist/provider-switcher.cjs +0 -95
  131. package/dist/provider-switcher.cjs.map +0 -1
  132. package/dist/provider-switcher.d.cts +0 -18
  133. package/dist/react/devtools-provider.cjs +0 -59
  134. package/dist/react/devtools-provider.cjs.map +0 -1
  135. package/dist/react/devtools-provider.d.cts +0 -39
  136. package/dist/react/use-fps-graph.cjs +0 -49
  137. package/dist/react/use-fps-graph.cjs.map +0 -1
  138. package/dist/react/use-fps-graph.d.cts +0 -15
  139. package/dist/react/use-pane-button.cjs +0 -58
  140. package/dist/react/use-pane-button.cjs.map +0 -1
  141. package/dist/react/use-pane-button.d.cts +0 -10
  142. package/dist/react/use-pane-folder.cjs +0 -48
  143. package/dist/react/use-pane-folder.cjs.map +0 -1
  144. package/dist/react/use-pane-folder.d.cts +0 -15
  145. package/dist/react/use-pane-input.cjs +0 -70
  146. package/dist/react/use-pane-input.cjs.map +0 -1
  147. package/dist/react/use-pane-input.d.cts +0 -44
  148. package/dist/react/use-pane-radio-grid.cjs +0 -78
  149. package/dist/react/use-pane-radio-grid.cjs.map +0 -1
  150. package/dist/react/use-pane-radio-grid.d.cts +0 -35
  151. package/dist/react/use-pane.cjs +0 -45
  152. package/dist/react/use-pane.cjs.map +0 -1
  153. package/dist/react/use-pane.d.cts +0 -40
  154. package/dist/react.cjs +0 -50
  155. package/dist/react.cjs.map +0 -1
  156. package/dist/react.d.cts +0 -12
  157. package/dist/react.js.map +0 -1
  158. package/dist/registry-view.cjs +0 -371
  159. package/dist/registry-view.cjs.map +0 -1
  160. package/dist/registry-view.d.cts +0 -24
  161. package/dist/stats-graph.cjs +0 -297
  162. package/dist/stats-graph.cjs.map +0 -1
  163. package/dist/stats-graph.d.cts +0 -40
  164. package/dist/stats-row.cjs +0 -153
  165. package/dist/stats-row.cjs.map +0 -1
  166. package/dist/stats-row.d.cts +0 -24
  167. package/dist/theme.cjs +0 -260
  168. package/dist/theme.cjs.map +0 -1
  169. package/dist/theme.d.cts +0 -10
  170. package/dist/vite-plugin.cjs +0 -113
  171. package/dist/vite-plugin.cjs.map +0 -1
  172. package/dist/vite-plugin.d.cts +0 -9
@@ -1 +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":[]}
1
+ {"version":3,"file":"use-pane-folder.js","names":[],"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,SAAgB,cACd,QACA,OACA,UAAiC,CAAC,GAChB;CAClB,MAAM,CAAC,QAAQ,aAAa,SAA2B,IAAI;CAK3D,MAAM,aAAa,qBAAqB,OAAO;CAE/C,sBAAsB;EACpB,IAAI,CAAC,QAAQ;GACX,UAAU,IAAI;GACd;EACF;EACA,MAAM,IAAI,OAAO,UAAU;GAAE,UAAU;GAAO,GAAG,WAAW;GAAG;EAAM,CAAC;EACtE,UAAU,CAAC;EACX,aAAa;GACX,IAAI;IACF,EAAE,QAAQ;GACZ,QAAQ,CAGR;EACF;CACF,GAAG,CAAC,QAAQ,KAAK,CAAC;CAElB,OAAO;AACT"}
@@ -1,33 +1,33 @@
1
- import { Pane, FolderApi } from 'tweakpane';
2
-
1
+ import { FolderApi, Pane } from "tweakpane";
2
+ //#region src/react/use-pane-input.d.ts
3
3
  type PaneParent = Pane | FolderApi;
4
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;
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
31
  }
32
32
  /**
33
33
  * Bind a Tweakpane input to React state.
@@ -40,5 +40,6 @@ interface PaneInputOptions {
40
40
  * re-binds to the new pane. Cleanup disposes the binding immediately.
41
41
  */
42
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 };
43
+ //#endregion
44
+ export { PaneInputOptions, PaneParent, usePaneInput };
45
+ //# sourceMappingURL=use-pane-input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-pane-input.d.ts","names":[],"sources":["../../src/react/use-pane-input.ts"],"mappings":";;KAGY,aAAa,OAAO;UAEf;;EAEf;;EAEA;;EAEA;;EAEA,UAAU;;EAEV;;EAEA;IAAU;;;EAEV;;;;;EAKA;;;;;EAKA,UAAU;;;;;;;;;;;;iBAaI,aAAa,GAC3B,QAAQ,mBACR,aACA,cAAc,GACd,UAAS,oBACP,IAAI,GAAG"}
@@ -1,46 +1,50 @@
1
1
  import { useCallback, useEffectEvent, useLayoutEffect, useRef, useState } from "react";
2
+ //#region src/react/use-pane-input.ts
3
+ /**
4
+ * Bind a Tweakpane input to React state.
5
+ * Returns [value, setValue] — setValue updates both React state and the
6
+ * Tweakpane binding.
7
+ *
8
+ * The binding is created in `useLayoutEffect` and rebuilt whenever
9
+ * `parent` or `key` changes — so when `usePane` tears down and rebuilds
10
+ * its bundle (StrictMode, true remount), this hook automatically
11
+ * re-binds to the new pane. Cleanup disposes the binding immediately.
12
+ */
2
13
  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];
14
+ const [value, setValueState] = useState(initialValue);
15
+ const paramsRef = useRef({ [key]: initialValue });
16
+ const bindingRef = useRef(null);
17
+ const getOptions = useEffectEvent(() => options);
18
+ const getValue = useEffectEvent(() => value);
19
+ useLayoutEffect(() => {
20
+ if (!parent) {
21
+ bindingRef.current = null;
22
+ return;
23
+ }
24
+ paramsRef.current[key] = getValue();
25
+ const { label, ...bindingOpts } = getOptions();
26
+ const binding = parent.addBinding(paramsRef.current, key, {
27
+ label: label ?? key,
28
+ ...bindingOpts
29
+ });
30
+ binding.on("change", (ev) => {
31
+ setValueState(ev.value);
32
+ });
33
+ bindingRef.current = binding;
34
+ return () => {
35
+ try {
36
+ binding.dispose();
37
+ } catch {}
38
+ if (bindingRef.current === binding) bindingRef.current = null;
39
+ };
40
+ }, [parent, key]);
41
+ return [value, useCallback((v) => {
42
+ paramsRef.current[key] = v;
43
+ setValueState(v);
44
+ bindingRef.current?.refresh();
45
+ }, [key])];
42
46
  }
43
- export {
44
- usePaneInput
45
- };
47
+ //#endregion
48
+ export { usePaneInput };
49
+
46
50
  //# sourceMappingURL=use-pane-input.js.map
@@ -1 +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":[]}
1
+ {"version":3,"file":"use-pane-input.js","names":[],"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 (bindingRef.current === (binding as unknown as { refresh(): void; dispose(): void })) {\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":";;;;;;;;;;;;AA0CA,SAAgB,aACd,QACA,KACA,cACA,UAA4B,CAAC,GACR;CACrB,MAAM,CAAC,OAAO,iBAAiB,SAAY,YAAY;CACvD,MAAM,YAAY,OAAgC,GAAG,MAAM,aAAa,CAAC;CACzE,MAAM,aAAa,OAAoD,IAAI;CAO3E,MAAM,aAAa,qBAAqB,OAAO;CAC/C,MAAM,WAAW,qBAAqB,KAAK;CAE3C,sBAAsB;EACpB,IAAI,CAAC,QAAQ;GACX,WAAW,UAAU;GACrB;EACF;EAIA,UAAU,QAAQ,OAAO,SAAS;EAClC,MAAM,EAAE,OAAO,GAAG,gBAAgB,WAAW;EAC7C,MAAM,UAAU,OAAO,WAAW,UAAU,SAAS,KAAK;GACxD,OAAO,SAAS;GAChB,GAAG;EACL,CAA4B;EAE5B,QAAQ,GAAG,WAAW,OAA2B;GAC/C,cAAc,GAAG,KAAU;EAC7B,CAAC;EACD,WAAW,UAAU;EAErB,aAAa;GACX,IAAI;IACF,QAAQ,QAAQ;GAClB,QAAQ,CAER;GACA,IAAI,WAAW,YAAa,SAC1B,WAAW,UAAU;EAEzB;CAGF,GAAG,CAAC,QAAQ,GAAG,CAAC;CAWhB,OAAO,CAAC,OATS,aACd,MAAS;EACR,UAAU,QAAQ,OAAO;EACzB,cAAc,CAAC;EACf,WAAW,SAAS,QAAQ;CAC9B,GACA,CAAC,GAAG,CAGgB,CAAC;AACzB"}
@@ -1,24 +1,23 @@
1
- import { PaneParent } from './use-pane-input.js';
2
- import 'tweakpane';
3
-
1
+ import { PaneParent } from "./use-pane-input.js";
2
+ //#region src/react/use-pane-radio-grid.d.ts
4
3
  interface PaneRadioGridCell<T> {
5
- /** Visible label on the button. */
6
- title: string;
7
- /** Value emitted by this cell when selected. */
8
- value: T;
4
+ /** Visible label on the button. */
5
+ title: string;
6
+ /** Value emitted by this cell when selected. */
7
+ value: T;
9
8
  }
10
9
  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];
10
+ /** Radio-group name. Required by Tweakpane — any unique string works. */
11
+ groupName?: string;
12
+ /** Initially-selected value. Must match one of `cells[i].value`. */
13
+ initialValue: T;
14
+ /**
15
+ * Grid cells in row-major order. Default layout is a single row with
16
+ * `cells.length` columns, matching a top-of-pane toggle bar.
17
+ */
18
+ cells: ReadonlyArray<PaneRadioGridCell<T>>;
19
+ /** Explicit `[cols, rows]`. Defaults to `[cells.length, 1]`. */
20
+ size?: [number, number];
22
21
  }
23
22
  /**
24
23
  * Render an inline button-bar selector backed by the Tweakpane Essentials
@@ -31,5 +30,6 @@ interface PaneRadioGridOptions<T> {
31
30
  * React strict-mode's synchronous cleanup/re-mount pair.
32
31
  */
33
32
  declare function usePaneRadioGrid<T>(parent: PaneParent | null, options: PaneRadioGridOptions<T>): [T, (v: T) => void];
34
-
35
- export { type PaneRadioGridCell, type PaneRadioGridOptions, usePaneRadioGrid };
33
+ //#endregion
34
+ export { PaneRadioGridCell, PaneRadioGridOptions, usePaneRadioGrid };
35
+ //# sourceMappingURL=use-pane-radio-grid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-pane-radio-grid.d.ts","names":[],"sources":["../../src/react/use-pane-radio-grid.ts"],"mappings":";;UAGiB,kBAAkB;;EAEjC;;EAEA,OAAO;;UAGQ,qBAAqB;;EAEpC;;EAEA,cAAc;;;;;EAKd,OAAO,cAAc,kBAAkB;;EAEvC;;;;;;;;;;;;iBAac,iBAAiB,GAAG,QAAQ,mBAAmB,SAAS,qBAAqB,MAAM,IAAI,GAAG"}
@@ -1,54 +1,76 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
+ //#region src/react/use-pane-radio-grid.ts
3
+ /**
4
+ * Render an inline button-bar selector backed by the Tweakpane Essentials
5
+ * `radiogrid` blade. Returns `[value, setValue]` — interacting with the
6
+ * pane buttons updates React state, and `setValue` pushes back into the
7
+ * blade. Intended for scene/mode toggles where a dropdown feels too
8
+ * heavyweight and a row of labeled buttons reads better.
9
+ *
10
+ * Disposal is deferred through `setTimeout(0)` so the blade survives
11
+ * React strict-mode's synchronous cleanup/re-mount pair.
12
+ */
2
13
  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];
14
+ const { cells, initialValue, groupName, size } = options;
15
+ const [value, setValueState] = useState(initialValue);
16
+ const bladeRef = useRef(null);
17
+ const cellsRef = useRef(cells);
18
+ cellsRef.current = cells;
19
+ const mountedRef = useRef(false);
20
+ const ensureBlade = useCallback(() => {
21
+ if (!parent || bladeRef.current) return;
22
+ const [cols, rows] = size ?? [cells.length, 1];
23
+ const bag = { value: initialValue };
24
+ const blade = parent.addBlade({
25
+ view: "radiogrid",
26
+ groupName: groupName ?? `radio-${Math.random().toString(36).slice(2, 10)}`,
27
+ size: [cols, rows],
28
+ cells: (x, y) => {
29
+ const idx = y * cols + x;
30
+ const cell = cellsRef.current[idx];
31
+ return cell ? {
32
+ title: cell.title,
33
+ value: cell.value
34
+ } : {
35
+ title: "",
36
+ value: void 0
37
+ };
38
+ },
39
+ value: bag.value
40
+ });
41
+ blade?.on("change", (ev) => {
42
+ setValueState(ev.value);
43
+ });
44
+ bladeRef.current = blade;
45
+ }, [
46
+ parent,
47
+ groupName,
48
+ initialValue,
49
+ cells.length,
50
+ size
51
+ ]);
52
+ if (parent && bladeRef.current === null) ensureBlade();
53
+ 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
+ return [value, useCallback((v) => {
68
+ setValueState(v);
69
+ const blade = bladeRef.current;
70
+ if (blade && "value" in blade) blade.value = v;
71
+ }, [])];
50
72
  }
51
- export {
52
- usePaneRadioGrid
53
- };
73
+ //#endregion
74
+ export { usePaneRadioGrid };
75
+
54
76
  //# sourceMappingURL=use-pane-radio-grid.js.map
@@ -1 +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":[]}
1
+ {"version":3,"file":"use-pane-radio-grid.js","names":[],"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>(parent: PaneParent | null, options: PaneRadioGridOptions<T>): [T, (v: T) => void] {\n const { cells, initialValue, groupName, size } = options\n const [value, setValueState] = useState<T>(initialValue)\n\n const bladeRef = useRef<{\n dispose(): void\n on: (ev: string, fn: (e: { value: T }) => void) => unknown\n value?: T\n } | 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 = (\n parent as unknown as {\n addBlade: (opts: Record<string, unknown>) => typeof bladeRef.current\n }\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":";;;;;;;;;;;;AAkCA,SAAgB,iBAAoB,QAA2B,SAAuD;CACpH,MAAM,EAAE,OAAO,cAAc,WAAW,SAAS;CACjD,MAAM,CAAC,OAAO,iBAAiB,SAAY,YAAY;CAEvD,MAAM,WAAW,OAIP,IAAI;CACd,MAAM,WAAW,OAAO,KAAK;CAC7B,SAAS,UAAU;CACnB,MAAM,aAAa,OAAO,KAAK;CAE/B,MAAM,cAAc,kBAAkB;EACpC,IAAI,CAAC,UAAU,SAAS,SAAS;EAEjC,MAAM,CAAC,MAAM,QAAQ,QAAQ,CAAC,MAAM,QAAQ,CAAC;EAC7C,MAAM,MAAM,EAAE,OAAO,aAAa;EAClC,MAAM,QACJ,OAGA,SAAS;GACT,MAAM;GACN,WAAW,aAAa,SAAS,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,EAAE;GACvE,MAAM,CAAC,MAAM,IAAI;GACjB,QAAQ,GAAW,MAAc;IAC/B,MAAM,MAAM,IAAI,OAAO;IACvB,MAAM,OAAO,SAAS,QAAQ;IAC9B,OAAO,OAAO;KAAE,OAAO,KAAK;KAAO,OAAO,KAAK;IAAM,IAAI;KAAE,OAAO;KAAI,OAAO,KAAA;IAAU;GACzF;GACA,OAAO,IAAI;EACb,CAAC;EAED,OAAO,GAAG,WAAW,OAAO;GAC1B,cAAc,GAAG,KAAK;EACxB,CAAC;EACD,SAAS,UAAU;CACrB,GAAG;EAAC;EAAQ;EAAW;EAAc,MAAM;EAAQ;CAAI,CAAC;CAGxD,IAAI,UAAU,SAAS,YAAY,MAAM,YAAY;CAErD,gBAAgB;EACd,WAAW,UAAU;EACrB,IAAI,UAAU,SAAS,YAAY,MAAM,YAAY;EAErD,aAAa;GACX,MAAM,QAAQ,SAAS;GACvB,iBAAiB;IACf,IAAI,CAAC,WAAW,WAAW,OAAO;KAChC,MAAM,QAAQ;KACd,IAAI,SAAS,YAAY,OAAO,SAAS,UAAU;IACrD;GACF,GAAG,CAAC;GACJ,WAAW,UAAU;EACvB;CAEF,GAAG,CAAC,MAAM,CAAC;CAQX,OAAO,CAAC,OANS,aAAa,MAAS;EACrC,cAAc,CAAC;EACf,MAAM,QAAQ,SAAS;EACvB,IAAI,SAAS,WAAW,OAAO,MAAM,QAAQ;CAC/C,GAAG,CAAC,CAEkB,CAAC;AACzB"}
@@ -1,6 +1,5 @@
1
- import { CreatePaneOptions, PaneBundle } from '../create-pane.js';
2
- import 'tweakpane';
3
-
1
+ import { CreatePaneOptions, PaneBundle } from "../create-pane.js";
2
+ //#region src/react/use-pane.d.ts
4
3
  /**
5
4
  * Create a themed Tweakpane instance. Disposes on unmount, recreates on
6
5
  * remount. Returns a `PaneBundle` whose identity changes if React tears
@@ -36,5 +35,6 @@ import 'tweakpane';
36
35
  * ```
37
36
  */
38
37
  declare function usePane(options?: CreatePaneOptions): PaneBundle;
39
-
38
+ //#endregion
40
39
  export { usePane };
40
+ //# sourceMappingURL=use-pane.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-pane.d.ts","names":[],"sources":["../../src/react/use-pane.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsCgB,QAAQ,UAAS,oBAAyB"}
@@ -1,21 +1,56 @@
1
- import { useEffect, useEffectEvent, useState } from "react";
2
1
  import { claimPane, createPane } from "../create-pane.js";
2
+ import { useEffect, useEffectEvent, useState } from "react";
3
+ //#region src/react/use-pane.ts
4
+ /**
5
+ * Create a themed Tweakpane instance. Disposes on unmount, recreates on
6
+ * remount. Returns a `PaneBundle` whose identity changes if React tears
7
+ * the component down and rebuilds it (StrictMode, parent remount,
8
+ * Suspense). Child hooks that depend on the pane (`usePaneFolder`,
9
+ * `usePaneInput`) re-bind when this happens.
10
+ *
11
+ * Lifecycle:
12
+ * - Render: lazy-creates the bundle into a ref so it's available
13
+ * synchronously on first paint. Render runs twice in StrictMode but
14
+ * the ref guard prevents double-creation. Orphans from aborted
15
+ * concurrent renders are reclaimed by `createPane`'s unclaimed-slot
16
+ * mechanism.
17
+ * - Effect: claims the bundle so unrelated `createPane` calls won't
18
+ * dispose it; cleanup disposes the pane unconditionally.
19
+ * - Effect re-mount (StrictMode or true remount): the bundle in the
20
+ * ref is now `disposed`, so we create a fresh one and `force()` a
21
+ * re-render so consumers receive the new identity.
22
+ *
23
+ * Uses `driver: 'raf'` so the stats graph self-ticks via its own
24
+ * `requestAnimationFrame` loop. This makes `usePane` work regardless
25
+ * of whether it's called inside or outside R3F's `<Canvas>` context.
26
+ *
27
+ * @param options - Same {@link CreatePaneOptions} as `createPane`.
28
+ * @returns A {@link PaneBundle}; its identity changes across teardown and
29
+ * rebuild so dependent hooks (`usePaneFolder`, `usePaneInput`) re-bind.
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * const { pane } = usePane()
34
+ * const folder = usePaneFolder(pane, 'Settings')
35
+ * const [speed] = usePaneInput(folder, 'speed', 1, { min: 0, max: 10 })
36
+ * ```
37
+ */
3
38
  function usePane(options = {}) {
4
- const [bundle, setBundle] = useState(() => createPane(options));
5
- const getOptions = useEffectEvent(() => options);
6
- useEffect(() => {
7
- if (bundle.disposed) {
8
- setBundle(createPane(getOptions()));
9
- return;
10
- }
11
- claimPane(bundle);
12
- return () => {
13
- bundle.pane.dispose();
14
- };
15
- }, [bundle]);
16
- return bundle;
39
+ const [bundle, setBundle] = useState(() => createPane(options));
40
+ const getOptions = useEffectEvent(() => options);
41
+ useEffect(() => {
42
+ if (bundle.disposed) {
43
+ setBundle(createPane(getOptions()));
44
+ return;
45
+ }
46
+ claimPane(bundle);
47
+ return () => {
48
+ bundle.pane.dispose();
49
+ };
50
+ }, [bundle]);
51
+ return bundle;
17
52
  }
18
- export {
19
- usePane
20
- };
53
+ //#endregion
54
+ export { usePane };
55
+
21
56
  //# sourceMappingURL=use-pane.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/use-pane.ts"],"sourcesContent":["import { useEffect, useEffectEvent, useState } from 'react'\nimport { claimPane, createPane } from '../create-pane.js'\nimport type { CreatePaneOptions, PaneBundle } from '../create-pane.js'\n\n/**\n * Create a themed Tweakpane instance. Disposes on unmount, recreates on\n * remount. Returns a `PaneBundle` whose identity changes if React tears\n * the component down and rebuilds it (StrictMode, parent remount,\n * Suspense). Child hooks that depend on the pane (`usePaneFolder`,\n * `usePaneInput`) re-bind when this happens.\n *\n * Lifecycle:\n * - Render: lazy-creates the bundle into a ref so it's available\n * synchronously on first paint. Render runs twice in StrictMode but\n * the ref guard prevents double-creation. Orphans from aborted\n * concurrent renders are reclaimed by `createPane`'s unclaimed-slot\n * mechanism.\n * - Effect: claims the bundle so unrelated `createPane` calls won't\n * dispose it; cleanup disposes the pane unconditionally.\n * - Effect re-mount (StrictMode or true remount): the bundle in the\n * ref is now `disposed`, so we create a fresh one and `force()` a\n * re-render so consumers receive the new identity.\n *\n * Uses `driver: 'raf'` so the stats graph self-ticks via its own\n * `requestAnimationFrame` loop. This makes `usePane` work regardless\n * of whether it's called inside or outside R3F's `<Canvas>` context.\n *\n * @param options - Same {@link CreatePaneOptions} as `createPane`.\n * @returns A {@link PaneBundle}; its identity changes across teardown and\n * rebuild so dependent hooks (`usePaneFolder`, `usePaneInput`) re-bind.\n *\n * @example\n * ```tsx\n * const { pane } = usePane()\n * const folder = usePaneFolder(pane, 'Settings')\n * const [speed] = usePaneInput(folder, 'speed', 1, { min: 0, max: 10 })\n * ```\n */\nexport function usePane(options: CreatePaneOptions = {}): PaneBundle {\n // Lazy initializer creates the bundle once, synchronously on first\n // render, so it's available for first paint. StrictMode's double-invoke\n // is absorbed by createPane's unclaimed-slot orphan disposal.\n const [bundle, setBundle] = useState(() => createPane(options))\n\n // Latest options read non-reactively when a disposed bundle is rebuilt.\n const getOptions = useEffectEvent(() => options)\n\n useEffect(() => {\n if (bundle.disposed) {\n setBundle(createPane(getOptions()))\n return\n }\n claimPane(bundle)\n return () => {\n bundle.pane.dispose()\n }\n }, [bundle])\n\n return bundle\n}\n"],"mappings":"AAAA,SAAS,WAAW,gBAAgB,gBAAgB;AACpD,SAAS,WAAW,kBAAkB;AAqC/B,SAAS,QAAQ,UAA6B,CAAC,GAAe;AAInE,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,MAAM,WAAW,OAAO,CAAC;AAG9D,QAAM,aAAa,eAAe,MAAM,OAAO;AAE/C,YAAU,MAAM;AACd,QAAI,OAAO,UAAU;AACnB,gBAAU,WAAW,WAAW,CAAC,CAAC;AAClC;AAAA,IACF;AACA,cAAU,MAAM;AAChB,WAAO,MAAM;AACX,aAAO,KAAK,QAAQ;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,SAAO;AACT;","names":[]}
1
+ {"version":3,"file":"use-pane.js","names":[],"sources":["../../src/react/use-pane.ts"],"sourcesContent":["import { useEffect, useEffectEvent, useState } from 'react'\nimport { claimPane, createPane } from '../create-pane.js'\nimport type { CreatePaneOptions, PaneBundle } from '../create-pane.js'\n\n/**\n * Create a themed Tweakpane instance. Disposes on unmount, recreates on\n * remount. Returns a `PaneBundle` whose identity changes if React tears\n * the component down and rebuilds it (StrictMode, parent remount,\n * Suspense). Child hooks that depend on the pane (`usePaneFolder`,\n * `usePaneInput`) re-bind when this happens.\n *\n * Lifecycle:\n * - Render: lazy-creates the bundle into a ref so it's available\n * synchronously on first paint. Render runs twice in StrictMode but\n * the ref guard prevents double-creation. Orphans from aborted\n * concurrent renders are reclaimed by `createPane`'s unclaimed-slot\n * mechanism.\n * - Effect: claims the bundle so unrelated `createPane` calls won't\n * dispose it; cleanup disposes the pane unconditionally.\n * - Effect re-mount (StrictMode or true remount): the bundle in the\n * ref is now `disposed`, so we create a fresh one and `force()` a\n * re-render so consumers receive the new identity.\n *\n * Uses `driver: 'raf'` so the stats graph self-ticks via its own\n * `requestAnimationFrame` loop. This makes `usePane` work regardless\n * of whether it's called inside or outside R3F's `<Canvas>` context.\n *\n * @param options - Same {@link CreatePaneOptions} as `createPane`.\n * @returns A {@link PaneBundle}; its identity changes across teardown and\n * rebuild so dependent hooks (`usePaneFolder`, `usePaneInput`) re-bind.\n *\n * @example\n * ```tsx\n * const { pane } = usePane()\n * const folder = usePaneFolder(pane, 'Settings')\n * const [speed] = usePaneInput(folder, 'speed', 1, { min: 0, max: 10 })\n * ```\n */\nexport function usePane(options: CreatePaneOptions = {}): PaneBundle {\n // Lazy initializer creates the bundle once, synchronously on first\n // render, so it's available for first paint. StrictMode's double-invoke\n // is absorbed by createPane's unclaimed-slot orphan disposal.\n const [bundle, setBundle] = useState(() => createPane(options))\n\n // Latest options read non-reactively when a disposed bundle is rebuilt.\n const getOptions = useEffectEvent(() => options)\n\n useEffect(() => {\n if (bundle.disposed) {\n setBundle(createPane(getOptions()))\n return\n }\n claimPane(bundle)\n return () => {\n bundle.pane.dispose()\n }\n }, [bundle])\n\n return bundle\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,QAAQ,UAA6B,CAAC,GAAe;CAInE,MAAM,CAAC,QAAQ,aAAa,eAAe,WAAW,OAAO,CAAC;CAG9D,MAAM,aAAa,qBAAqB,OAAO;CAE/C,gBAAgB;EACd,IAAI,OAAO,UAAU;GACnB,UAAU,WAAW,WAAW,CAAC,CAAC;GAClC;EACF;EACA,UAAU,MAAM;EAChB,aAAa;GACX,OAAO,KAAK,QAAQ;EACtB;CACF,GAAG,CAAC,MAAM,CAAC;CAEX,OAAO;AACT"}
package/dist/react.d.ts CHANGED
@@ -1,12 +1,10 @@
1
- export { usePane } from './react/use-pane.js';
2
- export { DevtoolsProvider, DevtoolsProviderProps } from './react/devtools-provider.js';
3
- export { PaneInputOptions, PaneParent, usePaneInput } from './react/use-pane-input.js';
4
- export { usePaneFolder } from './react/use-pane-folder.js';
5
- export { usePaneButton } from './react/use-pane-button.js';
6
- export { PaneRadioGridCell, PaneRadioGridOptions, usePaneRadioGrid } from './react/use-pane-radio-grid.js';
7
- export { FpsGraphHandle, useFpsGraph } from './react/use-fps-graph.js';
8
- export { CreatePaneOptions, PaneBundle } from './create-pane.js';
9
- export { DevtoolsClient, DevtoolsClientOptions, DevtoolsState, DevtoolsStateListener } from './devtools-client.js';
10
- import 'react';
11
- import 'tweakpane';
12
- import 'three-flatland/debug-protocol';
1
+ import { DevtoolsClient, DevtoolsClientOptions, DevtoolsState, DevtoolsStateListener } from "./devtools-client.js";
2
+ import { CreatePaneOptions, PaneBundle } from "./create-pane.js";
3
+ import { usePane } from "./react/use-pane.js";
4
+ import { DevtoolsProvider, DevtoolsProviderProps } from "./react/devtools-provider.js";
5
+ import { PaneInputOptions, PaneParent, usePaneInput } from "./react/use-pane-input.js";
6
+ import { usePaneFolder } from "./react/use-pane-folder.js";
7
+ import { usePaneButton } from "./react/use-pane-button.js";
8
+ import { PaneRadioGridCell, PaneRadioGridOptions, usePaneRadioGrid } from "./react/use-pane-radio-grid.js";
9
+ import { FpsGraphHandle, useFpsGraph } from "./react/use-fps-graph.js";
10
+ export { type CreatePaneOptions, DevtoolsClient, type DevtoolsClientOptions, DevtoolsProvider, type DevtoolsProviderProps, type DevtoolsState, type DevtoolsStateListener, type FpsGraphHandle, type PaneBundle, type PaneInputOptions, type PaneParent, type PaneRadioGridCell, type PaneRadioGridOptions, useFpsGraph, usePane, usePaneButton, usePaneFolder, usePaneInput, usePaneRadioGrid };
package/dist/react.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { DevtoolsClient } from "./devtools-client.js";
1
2
  import { usePane } from "./react/use-pane.js";
2
3
  import { DevtoolsProvider } from "./react/devtools-provider.js";
3
4
  import { usePaneInput } from "./react/use-pane-input.js";
@@ -5,15 +6,4 @@ import { usePaneFolder } from "./react/use-pane-folder.js";
5
6
  import { usePaneButton } from "./react/use-pane-button.js";
6
7
  import { usePaneRadioGrid } from "./react/use-pane-radio-grid.js";
7
8
  import { useFpsGraph } from "./react/use-fps-graph.js";
8
- import { DevtoolsClient } from "./devtools-client.js";
9
- export {
10
- DevtoolsClient,
11
- DevtoolsProvider,
12
- useFpsGraph,
13
- usePane,
14
- usePaneButton,
15
- usePaneFolder,
16
- usePaneInput,
17
- usePaneRadioGrid
18
- };
19
- //# sourceMappingURL=react.js.map
9
+ export { DevtoolsClient, DevtoolsProvider, useFpsGraph, usePane, usePaneButton, usePaneFolder, usePaneInput, usePaneRadioGrid };
@@ -1,24 +1,11 @@
1
- import { Pane, FolderApi } from 'tweakpane';
2
- import { DevtoolsClient } from './devtools-client.js';
3
- import 'three-flatland/debug-protocol';
4
-
5
- /**
6
- * Registry view — one group visible at a time. Header reads
7
- * `◀ groupName ▶`; arrows cycle through the groups the provider has
8
- * published. Below the header, every entry in the active group is
9
- * stacked (one bar chart per entry) so you see the full module in one
10
- * glance. Hidden while the registry is empty.
11
- *
12
- * Groups are derived from the part of each entry name before the
13
- * first `.` (`forwardPlus.lightCounts` + `forwardPlus.tileScores` →
14
- * group `forwardPlus`). Names without a dot land in an `ungrouped`
15
- * bucket.
16
- */
17
-
1
+ import { DevtoolsClient } from "./devtools-client.js";
2
+ import { FolderApi, Pane } from "tweakpane";
3
+ //#region src/registry-view.d.ts
18
4
  interface RegistryViewHandle {
19
- readonly element: HTMLElement;
20
- dispose(): void;
5
+ readonly element: HTMLElement;
6
+ dispose(): void;
21
7
  }
22
8
  declare function addRegistryView(parent: Pane | FolderApi, client: DevtoolsClient): RegistryViewHandle;
23
-
24
- export { type RegistryViewHandle, addRegistryView };
9
+ //#endregion
10
+ export { RegistryViewHandle, addRegistryView };
11
+ //# sourceMappingURL=registry-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry-view.d.ts","names":[],"sources":["../src/registry-view.ts"],"mappings":";;;UAiBiB;WACN,SAAS;EAClB;;iBAuCc,gBAAgB,QAAQ,OAAO,WAAW,QAAQ,iBAAiB"}