@nonsoanetoh/lil-gui-helper 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,131 @@
1
+ import * as React from 'react';
2
+
3
+ type GuiRoute = "*" | `/${string}`;
4
+ type GuiItemBase = {
5
+ id?: string;
6
+ name: string;
7
+ routes?: GuiRoute[];
8
+ };
9
+ type GuiItemNumber = GuiItemBase & {
10
+ type: "number";
11
+ value: number;
12
+ min?: number;
13
+ max?: number;
14
+ step?: number;
15
+ onChange: (value: number) => void;
16
+ };
17
+ type GuiItemColor = GuiItemBase & {
18
+ type: "color";
19
+ value: string;
20
+ onChange: (value: string) => void;
21
+ };
22
+ type GuiItemBoolean = GuiItemBase & {
23
+ type: "boolean";
24
+ value: boolean;
25
+ onChange: (value: boolean) => void;
26
+ };
27
+ type GuiItemString = GuiItemBase & {
28
+ type: "string";
29
+ value: string;
30
+ onChange: (value: string) => void;
31
+ };
32
+ type GuiItemDropdown = GuiItemBase & {
33
+ type: "dropdown";
34
+ value: string;
35
+ options: string[] | Record<string, string>;
36
+ onChange: (value: string) => void;
37
+ };
38
+ type GuiItemFolder = GuiItemBase & {
39
+ type: "folder";
40
+ children: GuiItem[];
41
+ };
42
+ type GuiItem = GuiItemNumber | GuiItemColor | GuiItemBoolean | GuiItemString | GuiItemDropdown | GuiItemFolder;
43
+ type SettingDefinitionValue = string | number | boolean;
44
+ type SettingDefinitionBase = {
45
+ id: string;
46
+ name: string;
47
+ default: SettingDefinitionValue;
48
+ routes?: GuiRoute[];
49
+ };
50
+ type SettingDefinition = (SettingDefinitionBase & {
51
+ type: "color";
52
+ }) | (SettingDefinitionBase & {
53
+ type: "number";
54
+ min?: number;
55
+ max?: number;
56
+ step?: number;
57
+ }) | (SettingDefinitionBase & {
58
+ type: "boolean";
59
+ }) | (SettingDefinitionBase & {
60
+ type: "string";
61
+ }) | (SettingDefinitionBase & {
62
+ type: "dropdown";
63
+ options: string[] | Record<string, string>;
64
+ });
65
+ type SettingDefinitionFolder = {
66
+ type: "folder";
67
+ name: string;
68
+ routes?: GuiRoute[];
69
+ children: SettingDefinition[];
70
+ };
71
+ type SettingDefinitionItem = SettingDefinition | SettingDefinitionFolder;
72
+ type SettingPrimitive = number | string | boolean;
73
+ type SettingValue = SettingPrimitive | SettingsGroup;
74
+ interface SettingsGroup {
75
+ [key: string]: SettingValue;
76
+ }
77
+ type Settings = Record<string, SettingsGroup>;
78
+ type SettingsContextValue = {
79
+ settings: Settings;
80
+ updateSetting: (path: string, value: SettingValue) => void;
81
+ getSetting: <T extends SettingValue = SettingValue>(path: string) => T | undefined;
82
+ getGroup: (name: string) => SettingsGroup;
83
+ };
84
+
85
+ type SettingsProviderProps = {
86
+ children: React.ReactNode;
87
+ initialSettings?: Partial<Settings>;
88
+ };
89
+ declare function SettingsProvider({ children, initialSettings }: SettingsProviderProps): React.FunctionComponentElement<React.ProviderProps<SettingsContextValue | null>>;
90
+ declare function useSettings(): SettingsContextValue;
91
+
92
+ declare function matchRoute(pathname: string, route: GuiRoute): boolean;
93
+ declare function getDefinitionsForRoute(definitions: SettingDefinitionItem[], pathname: string): SettingDefinitionItem[];
94
+ declare function getDefinition(definitions: SettingDefinitionItem[], id: string): SettingDefinition | undefined;
95
+
96
+ type GuiConfigProviderProps = {
97
+ definitions: SettingDefinitionItem[];
98
+ children: React.ReactNode;
99
+ };
100
+ declare function GuiConfigProvider({ definitions, children }: GuiConfigProviderProps): React.FunctionComponentElement<React.ProviderProps<SettingDefinitionItem[]>>;
101
+ declare function useGuiDefinitions(): SettingDefinitionItem[];
102
+ declare function useGetDefinition(): (id: string) => ReturnType<typeof getDefinition>;
103
+
104
+ type GuiProviderProps = {
105
+ children: React.ReactNode;
106
+ visible?: boolean;
107
+ };
108
+ declare function GuiProvider({ children, visible, }: GuiProviderProps): React.FunctionComponentElement<React.ProviderProps<boolean>>;
109
+ declare function useGuiVisible(): boolean;
110
+
111
+ declare function GuiManager(): React.DetailedReactHTMLElement<{
112
+ ref: React.RefObject<HTMLDivElement | null>;
113
+ style: {
114
+ position: "fixed";
115
+ top: number;
116
+ right: number;
117
+ zIndex: number;
118
+ };
119
+ }, HTMLDivElement> | null;
120
+
121
+ type UseSettingOptions<T extends SettingDefinitionValue = SettingDefinitionValue> = {
122
+ default?: T;
123
+ onChange?: (value: T) => void;
124
+ };
125
+ declare function useSetting<T extends SettingDefinitionValue = SettingDefinitionValue>(path: string, defaultOrOptions?: T | UseSettingOptions<T>, onChange?: (value: T) => void): T | undefined;
126
+
127
+ type GetSetting = (path: string) => unknown;
128
+ type UpdateSetting = (path: string, value: SettingDefinitionValue) => void;
129
+ declare function buildGuiItemsFromDefinitions(definitions: SettingDefinitionItem[], pathname: string, getSetting: GetSetting, updateSetting: UpdateSetting): GuiItem[];
130
+
131
+ export { GuiConfigProvider, type GuiConfigProviderProps, type GuiItem, GuiManager, GuiProvider, type GuiProviderProps, type GuiRoute, type SettingDefinition, type SettingDefinitionBase, type SettingDefinitionFolder, type SettingDefinitionItem, type SettingDefinitionValue, type SettingValue, type Settings, type SettingsContextValue, SettingsProvider, type SettingsProviderProps, type UseSettingOptions, buildGuiItemsFromDefinitions, getDefinition, getDefinitionsForRoute, matchRoute, useGetDefinition, useGuiDefinitions, useGuiVisible, useSetting, useSettings };
@@ -0,0 +1,131 @@
1
+ import * as React from 'react';
2
+
3
+ type GuiRoute = "*" | `/${string}`;
4
+ type GuiItemBase = {
5
+ id?: string;
6
+ name: string;
7
+ routes?: GuiRoute[];
8
+ };
9
+ type GuiItemNumber = GuiItemBase & {
10
+ type: "number";
11
+ value: number;
12
+ min?: number;
13
+ max?: number;
14
+ step?: number;
15
+ onChange: (value: number) => void;
16
+ };
17
+ type GuiItemColor = GuiItemBase & {
18
+ type: "color";
19
+ value: string;
20
+ onChange: (value: string) => void;
21
+ };
22
+ type GuiItemBoolean = GuiItemBase & {
23
+ type: "boolean";
24
+ value: boolean;
25
+ onChange: (value: boolean) => void;
26
+ };
27
+ type GuiItemString = GuiItemBase & {
28
+ type: "string";
29
+ value: string;
30
+ onChange: (value: string) => void;
31
+ };
32
+ type GuiItemDropdown = GuiItemBase & {
33
+ type: "dropdown";
34
+ value: string;
35
+ options: string[] | Record<string, string>;
36
+ onChange: (value: string) => void;
37
+ };
38
+ type GuiItemFolder = GuiItemBase & {
39
+ type: "folder";
40
+ children: GuiItem[];
41
+ };
42
+ type GuiItem = GuiItemNumber | GuiItemColor | GuiItemBoolean | GuiItemString | GuiItemDropdown | GuiItemFolder;
43
+ type SettingDefinitionValue = string | number | boolean;
44
+ type SettingDefinitionBase = {
45
+ id: string;
46
+ name: string;
47
+ default: SettingDefinitionValue;
48
+ routes?: GuiRoute[];
49
+ };
50
+ type SettingDefinition = (SettingDefinitionBase & {
51
+ type: "color";
52
+ }) | (SettingDefinitionBase & {
53
+ type: "number";
54
+ min?: number;
55
+ max?: number;
56
+ step?: number;
57
+ }) | (SettingDefinitionBase & {
58
+ type: "boolean";
59
+ }) | (SettingDefinitionBase & {
60
+ type: "string";
61
+ }) | (SettingDefinitionBase & {
62
+ type: "dropdown";
63
+ options: string[] | Record<string, string>;
64
+ });
65
+ type SettingDefinitionFolder = {
66
+ type: "folder";
67
+ name: string;
68
+ routes?: GuiRoute[];
69
+ children: SettingDefinition[];
70
+ };
71
+ type SettingDefinitionItem = SettingDefinition | SettingDefinitionFolder;
72
+ type SettingPrimitive = number | string | boolean;
73
+ type SettingValue = SettingPrimitive | SettingsGroup;
74
+ interface SettingsGroup {
75
+ [key: string]: SettingValue;
76
+ }
77
+ type Settings = Record<string, SettingsGroup>;
78
+ type SettingsContextValue = {
79
+ settings: Settings;
80
+ updateSetting: (path: string, value: SettingValue) => void;
81
+ getSetting: <T extends SettingValue = SettingValue>(path: string) => T | undefined;
82
+ getGroup: (name: string) => SettingsGroup;
83
+ };
84
+
85
+ type SettingsProviderProps = {
86
+ children: React.ReactNode;
87
+ initialSettings?: Partial<Settings>;
88
+ };
89
+ declare function SettingsProvider({ children, initialSettings }: SettingsProviderProps): React.FunctionComponentElement<React.ProviderProps<SettingsContextValue | null>>;
90
+ declare function useSettings(): SettingsContextValue;
91
+
92
+ declare function matchRoute(pathname: string, route: GuiRoute): boolean;
93
+ declare function getDefinitionsForRoute(definitions: SettingDefinitionItem[], pathname: string): SettingDefinitionItem[];
94
+ declare function getDefinition(definitions: SettingDefinitionItem[], id: string): SettingDefinition | undefined;
95
+
96
+ type GuiConfigProviderProps = {
97
+ definitions: SettingDefinitionItem[];
98
+ children: React.ReactNode;
99
+ };
100
+ declare function GuiConfigProvider({ definitions, children }: GuiConfigProviderProps): React.FunctionComponentElement<React.ProviderProps<SettingDefinitionItem[]>>;
101
+ declare function useGuiDefinitions(): SettingDefinitionItem[];
102
+ declare function useGetDefinition(): (id: string) => ReturnType<typeof getDefinition>;
103
+
104
+ type GuiProviderProps = {
105
+ children: React.ReactNode;
106
+ visible?: boolean;
107
+ };
108
+ declare function GuiProvider({ children, visible, }: GuiProviderProps): React.FunctionComponentElement<React.ProviderProps<boolean>>;
109
+ declare function useGuiVisible(): boolean;
110
+
111
+ declare function GuiManager(): React.DetailedReactHTMLElement<{
112
+ ref: React.RefObject<HTMLDivElement | null>;
113
+ style: {
114
+ position: "fixed";
115
+ top: number;
116
+ right: number;
117
+ zIndex: number;
118
+ };
119
+ }, HTMLDivElement> | null;
120
+
121
+ type UseSettingOptions<T extends SettingDefinitionValue = SettingDefinitionValue> = {
122
+ default?: T;
123
+ onChange?: (value: T) => void;
124
+ };
125
+ declare function useSetting<T extends SettingDefinitionValue = SettingDefinitionValue>(path: string, defaultOrOptions?: T | UseSettingOptions<T>, onChange?: (value: T) => void): T | undefined;
126
+
127
+ type GetSetting = (path: string) => unknown;
128
+ type UpdateSetting = (path: string, value: SettingDefinitionValue) => void;
129
+ declare function buildGuiItemsFromDefinitions(definitions: SettingDefinitionItem[], pathname: string, getSetting: GetSetting, updateSetting: UpdateSetting): GuiItem[];
130
+
131
+ export { GuiConfigProvider, type GuiConfigProviderProps, type GuiItem, GuiManager, GuiProvider, type GuiProviderProps, type GuiRoute, type SettingDefinition, type SettingDefinitionBase, type SettingDefinitionFolder, type SettingDefinitionItem, type SettingDefinitionValue, type SettingValue, type Settings, type SettingsContextValue, SettingsProvider, type SettingsProviderProps, type UseSettingOptions, buildGuiItemsFromDefinitions, getDefinition, getDefinitionsForRoute, matchRoute, useGetDefinition, useGuiDefinitions, useGuiVisible, useSetting, useSettings };
package/dist/index.js ADDED
@@ -0,0 +1,299 @@
1
+ "use strict";
2
+ "use client";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.tsx
32
+ var index_exports = {};
33
+ __export(index_exports, {
34
+ GuiConfigProvider: () => GuiConfigProvider,
35
+ GuiManager: () => GuiManager,
36
+ GuiProvider: () => GuiProvider,
37
+ SettingsProvider: () => SettingsProvider,
38
+ buildGuiItemsFromDefinitions: () => buildGuiItemsFromDefinitions,
39
+ getDefinition: () => getDefinition,
40
+ getDefinitionsForRoute: () => getDefinitionsForRoute,
41
+ matchRoute: () => matchRoute,
42
+ useGetDefinition: () => useGetDefinition,
43
+ useGuiDefinitions: () => useGuiDefinitions,
44
+ useGuiVisible: () => useGuiVisible,
45
+ useSetting: () => useSetting,
46
+ useSettings: () => useSettings
47
+ });
48
+ module.exports = __toCommonJS(index_exports);
49
+
50
+ // src/settings-context.tsx
51
+ var React = __toESM(require("react"));
52
+ function getByPath(obj, path) {
53
+ const parts = path.split(".").filter(Boolean);
54
+ let current = obj;
55
+ for (const key of parts) {
56
+ if (current == null || typeof current !== "object" || Array.isArray(current)) return void 0;
57
+ current = current[key];
58
+ }
59
+ return current;
60
+ }
61
+ function setByPath(obj, path, value) {
62
+ const parts = path.split(".").filter(Boolean);
63
+ if (parts.length === 0) return obj;
64
+ const result = { ...obj };
65
+ let current = result;
66
+ for (let i = 0; i < parts.length - 1; i++) {
67
+ const key = parts[i];
68
+ const next = current[key];
69
+ const nextObj = next != null && typeof next === "object" && !Array.isArray(next) ? { ...next } : {};
70
+ current[key] = nextObj;
71
+ current = nextObj;
72
+ }
73
+ current[parts[parts.length - 1]] = value;
74
+ return result;
75
+ }
76
+ var SettingsContext = React.createContext(null);
77
+ function SettingsProvider({ children, initialSettings = {} }) {
78
+ const [settings, setSettings] = React.useState(() => {
79
+ const result = {};
80
+ for (const key in initialSettings) {
81
+ const group = initialSettings[key];
82
+ if (group !== void 0) result[key] = group;
83
+ }
84
+ return result;
85
+ });
86
+ const updateSetting = React.useCallback((path, value2) => {
87
+ setSettings((prev) => setByPath(prev, path, value2));
88
+ }, []);
89
+ const getSetting = React.useCallback(
90
+ (path) => getByPath(settings, path),
91
+ [settings]
92
+ );
93
+ const getGroup = React.useCallback((name) => settings[name] ?? {}, [settings]);
94
+ const value = React.useMemo(
95
+ () => ({ settings, updateSetting, getSetting, getGroup }),
96
+ [settings, updateSetting, getSetting, getGroup]
97
+ );
98
+ return React.createElement(SettingsContext.Provider, { value }, children);
99
+ }
100
+ function useSettings() {
101
+ const ctx = React.useContext(SettingsContext);
102
+ if (!ctx) throw new Error("useSettings must be used within SettingsProvider");
103
+ return ctx;
104
+ }
105
+
106
+ // src/config-context.tsx
107
+ var React2 = __toESM(require("react"));
108
+
109
+ // src/routes.ts
110
+ function matchRoute(pathname, route) {
111
+ if (route === "*") return true;
112
+ if (route.endsWith("/*")) {
113
+ const prefix = route.slice(0, -2);
114
+ return pathname.startsWith(prefix + "/");
115
+ }
116
+ return pathname === route;
117
+ }
118
+ function getDefinitionsForRoute(definitions, pathname) {
119
+ return definitions.filter((def) => {
120
+ const routes = def.routes;
121
+ if (!routes?.length) return true;
122
+ return routes.some((r) => matchRoute(pathname, r));
123
+ });
124
+ }
125
+ function flatten(items, acc = /* @__PURE__ */ new Map()) {
126
+ for (const d of items) {
127
+ if (d.type === "folder") flatten(d.children, acc);
128
+ else if (d.id != null) acc.set(d.id, d);
129
+ }
130
+ return acc;
131
+ }
132
+ function getDefinition(definitions, id) {
133
+ return flatten(definitions).get(id);
134
+ }
135
+
136
+ // src/config-context.tsx
137
+ var DefinitionsContext = React2.createContext([]);
138
+ function GuiConfigProvider({ definitions, children }) {
139
+ const value = React2.useMemo(() => definitions, [definitions]);
140
+ return React2.createElement(DefinitionsContext.Provider, { value }, children);
141
+ }
142
+ function useGuiDefinitions() {
143
+ return React2.useContext(DefinitionsContext);
144
+ }
145
+ function useGetDefinition() {
146
+ const definitions = useGuiDefinitions();
147
+ return React2.useCallback((id) => getDefinition(definitions, id), [definitions]);
148
+ }
149
+
150
+ // src/gui-provider.tsx
151
+ var React3 = __toESM(require("react"));
152
+ var GuiVisibilityContext = React3.createContext(false);
153
+ function GuiProvider({
154
+ children,
155
+ visible = true
156
+ }) {
157
+ const value = React3.useMemo(() => !!visible, [visible]);
158
+ return React3.createElement(GuiVisibilityContext.Provider, { value }, children);
159
+ }
160
+ function useGuiVisible() {
161
+ return React3.useContext(GuiVisibilityContext);
162
+ }
163
+
164
+ // src/gui-manager.tsx
165
+ var React4 = __toESM(require("react"));
166
+ var import_navigation = require("next/navigation");
167
+ var import_lil_gui = __toESM(require("lil-gui"));
168
+
169
+ // src/builder.ts
170
+ function buildGuiItemsFromDefinitions(definitions, pathname, getSetting, updateSetting) {
171
+ const forRoute = getDefinitionsForRoute(definitions, pathname);
172
+ return forRoute.map((def) => definitionToGuiItem(def, getSetting, updateSetting));
173
+ }
174
+ function definitionToGuiItem(def, getSetting, updateSetting) {
175
+ if (def.type === "folder") {
176
+ return {
177
+ type: "folder",
178
+ name: def.name,
179
+ children: def.children.map((c) => definitionToGuiItem(c, getSetting, updateSetting))
180
+ };
181
+ }
182
+ const value = getSetting(def.id) ?? def.default;
183
+ switch (def.type) {
184
+ case "color":
185
+ return { type: "color", id: def.id, name: def.name, value, onChange: (v) => updateSetting(def.id, v) };
186
+ case "number":
187
+ return { type: "number", id: def.id, name: def.name, value, min: def.min, max: def.max, step: def.step, onChange: (v) => updateSetting(def.id, v) };
188
+ case "boolean":
189
+ return { type: "boolean", id: def.id, name: def.name, value, onChange: (v) => updateSetting(def.id, v) };
190
+ case "string":
191
+ return { type: "string", id: def.id, name: def.name, value, onChange: (v) => updateSetting(def.id, v) };
192
+ case "dropdown":
193
+ return { type: "dropdown", id: def.id, name: def.name, value, options: def.options, onChange: (v) => updateSetting(def.id, v) };
194
+ }
195
+ }
196
+
197
+ // src/gui-manager.tsx
198
+ function addItem(gui, item) {
199
+ if (item.type === "folder") {
200
+ const folder = gui.addFolder(item.name);
201
+ for (const child of item.children) addItem(folder, child);
202
+ return;
203
+ }
204
+ const obj = { value: item.value };
205
+ let controller;
206
+ switch (item.type) {
207
+ case "number":
208
+ controller = gui.add(obj, "value", item.min ?? 0, item.max ?? 100, item.step ?? 1);
209
+ break;
210
+ case "color":
211
+ controller = gui.addColor(obj, "value");
212
+ break;
213
+ case "boolean":
214
+ controller = gui.add(obj, "value");
215
+ break;
216
+ case "string":
217
+ controller = gui.add(obj, "value");
218
+ break;
219
+ case "dropdown":
220
+ controller = gui.add(obj, "value", item.options);
221
+ break;
222
+ default:
223
+ return;
224
+ }
225
+ controller.name(item.name);
226
+ controller.onChange((value) => {
227
+ item.onChange(value);
228
+ });
229
+ }
230
+ function GuiManager() {
231
+ const pathname = (0, import_navigation.usePathname)();
232
+ const visible = useGuiVisible();
233
+ const definitions = useGuiDefinitions();
234
+ const { getSetting, updateSetting } = useSettings();
235
+ const containerRef = React4.useRef(null);
236
+ const getSettingRef = React4.useRef(getSetting);
237
+ const updateSettingRef = React4.useRef(updateSetting);
238
+ React4.useEffect(() => {
239
+ getSettingRef.current = getSetting;
240
+ updateSettingRef.current = updateSetting;
241
+ }, [getSetting, updateSetting]);
242
+ React4.useEffect(() => {
243
+ if (!visible || !definitions.length) return;
244
+ const container = containerRef.current;
245
+ if (!container) return;
246
+ const get = (path) => getSettingRef.current(path);
247
+ const set = (path, value) => updateSettingRef.current(path, value);
248
+ const items = buildGuiItemsFromDefinitions(definitions, pathname, get, set);
249
+ if (items.length === 0) return;
250
+ const gui = new import_lil_gui.default({ container, title: "Settings", autoPlace: false });
251
+ for (const item of items) addItem(gui, item);
252
+ return () => gui.destroy();
253
+ }, [pathname, visible, definitions]);
254
+ if (!visible) return null;
255
+ return React4.createElement("div", {
256
+ ref: containerRef,
257
+ style: { position: "fixed", top: 16, right: 16, zIndex: 9999 }
258
+ });
259
+ }
260
+
261
+ // src/use-setting.ts
262
+ var React5 = __toESM(require("react"));
263
+ function useSetting(path, defaultOrOptions, onChange) {
264
+ const { getSetting } = useSettings();
265
+ const visible = useGuiVisible();
266
+ const getDef = useGetDefinition();
267
+ const options = defaultOrOptions != null && typeof defaultOrOptions === "object" && !Array.isArray(defaultOrOptions) && "default" in defaultOrOptions ? defaultOrOptions : { default: defaultOrOptions, onChange };
268
+ const defaultVal = options.default ?? getDef(path)?.default;
269
+ const onChangeFn = options.onChange ?? onChange;
270
+ const raw = getSetting(path);
271
+ const value = visible ? raw ?? defaultVal : void 0;
272
+ const prevRef = React5.useRef(value);
273
+ React5.useEffect(() => {
274
+ if (value === void 0) return;
275
+ if (value !== prevRef.current) {
276
+ prevRef.current = value;
277
+ onChangeFn?.(value);
278
+ } else {
279
+ prevRef.current = value;
280
+ }
281
+ }, [value, onChangeFn]);
282
+ return value;
283
+ }
284
+ // Annotate the CommonJS export names for ESM import in node:
285
+ 0 && (module.exports = {
286
+ GuiConfigProvider,
287
+ GuiManager,
288
+ GuiProvider,
289
+ SettingsProvider,
290
+ buildGuiItemsFromDefinitions,
291
+ getDefinition,
292
+ getDefinitionsForRoute,
293
+ matchRoute,
294
+ useGetDefinition,
295
+ useGuiDefinitions,
296
+ useGuiVisible,
297
+ useSetting,
298
+ useSettings
299
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,251 @@
1
+ "use client";
2
+
3
+ // src/settings-context.tsx
4
+ import * as React from "react";
5
+ function getByPath(obj, path) {
6
+ const parts = path.split(".").filter(Boolean);
7
+ let current = obj;
8
+ for (const key of parts) {
9
+ if (current == null || typeof current !== "object" || Array.isArray(current)) return void 0;
10
+ current = current[key];
11
+ }
12
+ return current;
13
+ }
14
+ function setByPath(obj, path, value) {
15
+ const parts = path.split(".").filter(Boolean);
16
+ if (parts.length === 0) return obj;
17
+ const result = { ...obj };
18
+ let current = result;
19
+ for (let i = 0; i < parts.length - 1; i++) {
20
+ const key = parts[i];
21
+ const next = current[key];
22
+ const nextObj = next != null && typeof next === "object" && !Array.isArray(next) ? { ...next } : {};
23
+ current[key] = nextObj;
24
+ current = nextObj;
25
+ }
26
+ current[parts[parts.length - 1]] = value;
27
+ return result;
28
+ }
29
+ var SettingsContext = React.createContext(null);
30
+ function SettingsProvider({ children, initialSettings = {} }) {
31
+ const [settings, setSettings] = React.useState(() => {
32
+ const result = {};
33
+ for (const key in initialSettings) {
34
+ const group = initialSettings[key];
35
+ if (group !== void 0) result[key] = group;
36
+ }
37
+ return result;
38
+ });
39
+ const updateSetting = React.useCallback((path, value2) => {
40
+ setSettings((prev) => setByPath(prev, path, value2));
41
+ }, []);
42
+ const getSetting = React.useCallback(
43
+ (path) => getByPath(settings, path),
44
+ [settings]
45
+ );
46
+ const getGroup = React.useCallback((name) => settings[name] ?? {}, [settings]);
47
+ const value = React.useMemo(
48
+ () => ({ settings, updateSetting, getSetting, getGroup }),
49
+ [settings, updateSetting, getSetting, getGroup]
50
+ );
51
+ return React.createElement(SettingsContext.Provider, { value }, children);
52
+ }
53
+ function useSettings() {
54
+ const ctx = React.useContext(SettingsContext);
55
+ if (!ctx) throw new Error("useSettings must be used within SettingsProvider");
56
+ return ctx;
57
+ }
58
+
59
+ // src/config-context.tsx
60
+ import * as React2 from "react";
61
+
62
+ // src/routes.ts
63
+ function matchRoute(pathname, route) {
64
+ if (route === "*") return true;
65
+ if (route.endsWith("/*")) {
66
+ const prefix = route.slice(0, -2);
67
+ return pathname.startsWith(prefix + "/");
68
+ }
69
+ return pathname === route;
70
+ }
71
+ function getDefinitionsForRoute(definitions, pathname) {
72
+ return definitions.filter((def) => {
73
+ const routes = def.routes;
74
+ if (!routes?.length) return true;
75
+ return routes.some((r) => matchRoute(pathname, r));
76
+ });
77
+ }
78
+ function flatten(items, acc = /* @__PURE__ */ new Map()) {
79
+ for (const d of items) {
80
+ if (d.type === "folder") flatten(d.children, acc);
81
+ else if (d.id != null) acc.set(d.id, d);
82
+ }
83
+ return acc;
84
+ }
85
+ function getDefinition(definitions, id) {
86
+ return flatten(definitions).get(id);
87
+ }
88
+
89
+ // src/config-context.tsx
90
+ var DefinitionsContext = React2.createContext([]);
91
+ function GuiConfigProvider({ definitions, children }) {
92
+ const value = React2.useMemo(() => definitions, [definitions]);
93
+ return React2.createElement(DefinitionsContext.Provider, { value }, children);
94
+ }
95
+ function useGuiDefinitions() {
96
+ return React2.useContext(DefinitionsContext);
97
+ }
98
+ function useGetDefinition() {
99
+ const definitions = useGuiDefinitions();
100
+ return React2.useCallback((id) => getDefinition(definitions, id), [definitions]);
101
+ }
102
+
103
+ // src/gui-provider.tsx
104
+ import * as React3 from "react";
105
+ var GuiVisibilityContext = React3.createContext(false);
106
+ function GuiProvider({
107
+ children,
108
+ visible = true
109
+ }) {
110
+ const value = React3.useMemo(() => !!visible, [visible]);
111
+ return React3.createElement(GuiVisibilityContext.Provider, { value }, children);
112
+ }
113
+ function useGuiVisible() {
114
+ return React3.useContext(GuiVisibilityContext);
115
+ }
116
+
117
+ // src/gui-manager.tsx
118
+ import * as React4 from "react";
119
+ import { usePathname } from "next/navigation";
120
+ import GUI from "lil-gui";
121
+
122
+ // src/builder.ts
123
+ function buildGuiItemsFromDefinitions(definitions, pathname, getSetting, updateSetting) {
124
+ const forRoute = getDefinitionsForRoute(definitions, pathname);
125
+ return forRoute.map((def) => definitionToGuiItem(def, getSetting, updateSetting));
126
+ }
127
+ function definitionToGuiItem(def, getSetting, updateSetting) {
128
+ if (def.type === "folder") {
129
+ return {
130
+ type: "folder",
131
+ name: def.name,
132
+ children: def.children.map((c) => definitionToGuiItem(c, getSetting, updateSetting))
133
+ };
134
+ }
135
+ const value = getSetting(def.id) ?? def.default;
136
+ switch (def.type) {
137
+ case "color":
138
+ return { type: "color", id: def.id, name: def.name, value, onChange: (v) => updateSetting(def.id, v) };
139
+ case "number":
140
+ return { type: "number", id: def.id, name: def.name, value, min: def.min, max: def.max, step: def.step, onChange: (v) => updateSetting(def.id, v) };
141
+ case "boolean":
142
+ return { type: "boolean", id: def.id, name: def.name, value, onChange: (v) => updateSetting(def.id, v) };
143
+ case "string":
144
+ return { type: "string", id: def.id, name: def.name, value, onChange: (v) => updateSetting(def.id, v) };
145
+ case "dropdown":
146
+ return { type: "dropdown", id: def.id, name: def.name, value, options: def.options, onChange: (v) => updateSetting(def.id, v) };
147
+ }
148
+ }
149
+
150
+ // src/gui-manager.tsx
151
+ function addItem(gui, item) {
152
+ if (item.type === "folder") {
153
+ const folder = gui.addFolder(item.name);
154
+ for (const child of item.children) addItem(folder, child);
155
+ return;
156
+ }
157
+ const obj = { value: item.value };
158
+ let controller;
159
+ switch (item.type) {
160
+ case "number":
161
+ controller = gui.add(obj, "value", item.min ?? 0, item.max ?? 100, item.step ?? 1);
162
+ break;
163
+ case "color":
164
+ controller = gui.addColor(obj, "value");
165
+ break;
166
+ case "boolean":
167
+ controller = gui.add(obj, "value");
168
+ break;
169
+ case "string":
170
+ controller = gui.add(obj, "value");
171
+ break;
172
+ case "dropdown":
173
+ controller = gui.add(obj, "value", item.options);
174
+ break;
175
+ default:
176
+ return;
177
+ }
178
+ controller.name(item.name);
179
+ controller.onChange((value) => {
180
+ item.onChange(value);
181
+ });
182
+ }
183
+ function GuiManager() {
184
+ const pathname = usePathname();
185
+ const visible = useGuiVisible();
186
+ const definitions = useGuiDefinitions();
187
+ const { getSetting, updateSetting } = useSettings();
188
+ const containerRef = React4.useRef(null);
189
+ const getSettingRef = React4.useRef(getSetting);
190
+ const updateSettingRef = React4.useRef(updateSetting);
191
+ React4.useEffect(() => {
192
+ getSettingRef.current = getSetting;
193
+ updateSettingRef.current = updateSetting;
194
+ }, [getSetting, updateSetting]);
195
+ React4.useEffect(() => {
196
+ if (!visible || !definitions.length) return;
197
+ const container = containerRef.current;
198
+ if (!container) return;
199
+ const get = (path) => getSettingRef.current(path);
200
+ const set = (path, value) => updateSettingRef.current(path, value);
201
+ const items = buildGuiItemsFromDefinitions(definitions, pathname, get, set);
202
+ if (items.length === 0) return;
203
+ const gui = new GUI({ container, title: "Settings", autoPlace: false });
204
+ for (const item of items) addItem(gui, item);
205
+ return () => gui.destroy();
206
+ }, [pathname, visible, definitions]);
207
+ if (!visible) return null;
208
+ return React4.createElement("div", {
209
+ ref: containerRef,
210
+ style: { position: "fixed", top: 16, right: 16, zIndex: 9999 }
211
+ });
212
+ }
213
+
214
+ // src/use-setting.ts
215
+ import * as React5 from "react";
216
+ function useSetting(path, defaultOrOptions, onChange) {
217
+ const { getSetting } = useSettings();
218
+ const visible = useGuiVisible();
219
+ const getDef = useGetDefinition();
220
+ const options = defaultOrOptions != null && typeof defaultOrOptions === "object" && !Array.isArray(defaultOrOptions) && "default" in defaultOrOptions ? defaultOrOptions : { default: defaultOrOptions, onChange };
221
+ const defaultVal = options.default ?? getDef(path)?.default;
222
+ const onChangeFn = options.onChange ?? onChange;
223
+ const raw = getSetting(path);
224
+ const value = visible ? raw ?? defaultVal : void 0;
225
+ const prevRef = React5.useRef(value);
226
+ React5.useEffect(() => {
227
+ if (value === void 0) return;
228
+ if (value !== prevRef.current) {
229
+ prevRef.current = value;
230
+ onChangeFn?.(value);
231
+ } else {
232
+ prevRef.current = value;
233
+ }
234
+ }, [value, onChangeFn]);
235
+ return value;
236
+ }
237
+ export {
238
+ GuiConfigProvider,
239
+ GuiManager,
240
+ GuiProvider,
241
+ SettingsProvider,
242
+ buildGuiItemsFromDefinitions,
243
+ getDefinition,
244
+ getDefinitionsForRoute,
245
+ matchRoute,
246
+ useGetDefinition,
247
+ useGuiDefinitions,
248
+ useGuiVisible,
249
+ useSetting,
250
+ useSettings
251
+ };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@nonsoanetoh/lil-gui-helper",
3
+ "version": "0.1.0",
4
+ "description": "A Route-aware lil-gui manager for Next.js with a central config and useSetting hook",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "peerDependencies": {
19
+ "next": ">=14.0.0",
20
+ "react": ">=18.0.0",
21
+ "react-dom": ">=18.0.0"
22
+ },
23
+ "dependencies": {
24
+ "lil-gui": "^0.21.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^20.0.0",
28
+ "@types/react": "^19.0.0",
29
+ "@types/react-dom": "^19.0.0",
30
+ "next": ">=14.0.0",
31
+ "react": "^19.0.0",
32
+ "react-dom": "^19.0.0",
33
+ "tsup": "^8.0.0",
34
+ "typescript": "^5.0.0"
35
+ },
36
+ "keywords": [
37
+ "next",
38
+ "lil-gui",
39
+ "gui",
40
+ "settings",
41
+ "dev-tools"
42
+ ],
43
+ "license": "MIT",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/nonsoanetoh/lil-gui-helper.git"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "scripts": {
52
+ "build": "tsup src/index.tsx --format cjs,esm --dts --clean",
53
+ "dev": "tsup src/index.tsx --format cjs,esm --dts --watch"
54
+ }
55
+ }
package/readme.md ADDED
@@ -0,0 +1,156 @@
1
+ # lil-gui-helper
2
+
3
+ Route-aware [lil-gui](https://lil-gui.georgealways.com/) manager for Next.js App Router: central config, optional visibility flag, and `useSetting` hook.
4
+
5
+ ## Install
6
+
7
+ From npm (when published):
8
+
9
+ ```bash
10
+ pnpm add lil-gui-helper
11
+ # or
12
+ npm i lil-gui-helper
13
+ ```
14
+
15
+ From a **private GitHub repo** (see [Publishing to a private repo](#publishing-to-a-private-repo) below):
16
+
17
+ ```bash
18
+ pnpm add git+https://github.com/YOUR_USERNAME/lil-gui-helper.git
19
+ # or with SSH
20
+ pnpm add git+ssh://git@github.com:YOUR_USERNAME/lil-gui-helper.git
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### 1. Define your config
26
+
27
+ Create a config array (e.g. `lib/gui-config.ts`):
28
+
29
+ ```ts
30
+ import type { SettingDefinitionItem } from "lil-gui-helper";
31
+
32
+ export const GUI_DEFINITIONS: SettingDefinitionItem[] = [
33
+ {
34
+ id: "home.backgroundColor",
35
+ type: "color",
36
+ name: "Background Color",
37
+ default: "#fefefe",
38
+ routes: ["/"],
39
+ },
40
+ {
41
+ type: "folder",
42
+ name: "Position",
43
+ routes: ["/work/*"],
44
+ children: [
45
+ { id: "position.x", type: "number", name: "X", default: 0 },
46
+ { id: "position.y", type: "number", name: "Y", default: 0 },
47
+ ],
48
+ },
49
+ ];
50
+ ```
51
+
52
+ **Routes:** `"*"` = all; `"/work"` = exact; `"/work/*"` = subroutes only (not `/work`).
53
+
54
+ ### 2. Wrap the app
55
+
56
+ In your root layout (inside `body`):
57
+
58
+ ```tsx
59
+ import {
60
+ SettingsProvider,
61
+ GuiConfigProvider,
62
+ GuiProvider,
63
+ GuiManager,
64
+ } from "lil-gui-helper";
65
+ import { GUI_DEFINITIONS } from "@/lib/gui-config";
66
+
67
+ export default function RootLayout({ children }) {
68
+ return (
69
+ <html>
70
+ <body>
71
+ <SettingsProvider>
72
+ <GuiConfigProvider definitions={GUI_DEFINITIONS}>
73
+ <GuiProvider visible={process.env.NODE_ENV === "development"}>
74
+ {children}
75
+ <GuiManager />
76
+ </GuiProvider>
77
+ </GuiConfigProvider>
78
+ </SettingsProvider>
79
+ </body>
80
+ </html>
81
+ );
82
+ }
83
+ ```
84
+
85
+ - **SettingsProvider** – stores values updated by the GUI.
86
+ - **GuiConfigProvider** – provides your definitions (required for `GuiManager` and `useSetting` defaults).
87
+ - **GuiProvider** – `visible`: when `false`, the panel is hidden and `useSetting` returns `undefined` so consumers use their default. Pass e.g. `visible={process.env.NODE_ENV === "development"}` to show only in dev.
88
+ - **GuiManager** – builds and mounts the lil-gui panel from definitions + current pathname.
89
+
90
+ ### 3. Read values in pages
91
+
92
+ ```tsx
93
+ "use client";
94
+
95
+ import { useSetting } from "lil-gui-helper";
96
+
97
+ export default function Home() {
98
+ const bg = useSetting("home.backgroundColor", "#fefefe");
99
+
100
+ return (
101
+ <div style={{ minHeight: "100vh", backgroundColor: bg ?? "#fefefe" }}>
102
+
103
+ </div>
104
+ );
105
+ }
106
+ ```
107
+
108
+ - `useSetting(path, default?)` – when GUI is visible returns the current value (or default); when hidden returns `undefined` so `value ?? default` uses your fallback.
109
+ - `useSetting(path, { default, onChange })` – optional `onChange` when the value changes.
110
+
111
+ ## API
112
+
113
+ | Export | Description |
114
+ | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
115
+ | `SettingsProvider` | Holds settings state; required above GUI. |
116
+ | `GuiConfigProvider` | Injects your `definitions`; required for `GuiManager` and definition-based defaults in `useSetting`. |
117
+ | `GuiProvider` | `visible` flag; when false, panel is hidden and `useSetting` is undefined. |
118
+ | `GuiManager` | Renders the lil-gui panel from definitions + pathname. |
119
+ | `useSetting(path, default?, onChange?)` | Current value or undefined when GUI hidden. |
120
+ | `useSettings()` | Raw getSetting / updateSetting. |
121
+ | `matchRoute(pathname, route)` | `"*"` \| `"/path"` \| `"/path/*"`. |
122
+ | `getDefinitionsForRoute(definitions, pathname)` | Filter definitions by pathname. |
123
+ | `getDefinition(definitions, id)` | Get leaf definition by id. |
124
+ | `buildGuiItemsFromDefinitions(definitions, pathname, getSetting, updateSetting)` | Build lil-gui items (for custom UIs). |
125
+
126
+ ## Publishing to a private repo
127
+
128
+ ### Option A: Private GitHub repository
129
+
130
+ 1. Create a **new private repo** on GitHub (e.g. `lil-gui-helper`).
131
+ 2. Update `package.json` in this package: set `repository.url` to your repo URL, e.g. `"url": "https://github.com/YOUR_USERNAME/lil-gui-helper.git"`.
132
+ 3. From this monorepo, copy only the package into the new repo (or push the whole `packages/lil-gui-helper` folder as the root of the new repo):
133
+ - Easiest: create the repo, then push the contents of `packages/lil-gui-helper` as the root (so the new repo has `package.json`, `src/`, `README.md`, etc. at top level).
134
+ 4. In the new repo, run `pnpm install` and `pnpm build`.
135
+ 5. In other projects, install from Git:
136
+ ```bash
137
+ pnpm add git+https://github.com/YOUR_USERNAME/lil-gui-helper.git
138
+ ```
139
+ For a private repo you’ll need auth (SSH key, or a personal access token in the URL, or `npm config set` for Git credentials).
140
+
141
+ ### Option B: Private npm package (scoped)
142
+
143
+ 1. Use a scoped name in `package.json`, e.g. `"name": "@your-npm-username/lil-gui-helper"`.
144
+ 2. Build: `cd packages/lil-gui-helper && pnpm build`.
145
+ 3. Publish as private (requires an npm paid plan for private packages):
146
+ ```bash
147
+ npm login
148
+ npm publish --access restricted
149
+ ```
150
+ Or with a scope that’s configured for private: `npm publish`.
151
+
152
+ 4. Install in other projects: `pnpm add @your-npm-username/lil-gui-helper`.
153
+
154
+ ---
155
+
156
+ **Next step:** Open a new tab, create the private GitHub repo, then we can wire the `repository` URL and decide whether you’re using Option A (install from Git) or Option B (private npm).