{"id":17594857,"url":"https://github.com/braebo/gooey","last_synced_at":"2025-06-14T21:03:13.919Z","repository":{"id":247268654,"uuid":"825401893","full_name":"braebo/gooey","owner":"braebo","description":"floating gui library for the web","archived":false,"fork":false,"pushed_at":"2024-09-23T14:50:40.000Z","size":10655,"stargazers_count":5,"open_issues_count":17,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-30T04:42:48.130Z","etag":null,"topics":["draggable","gui","presets","typescript","window-manager"],"latest_commit_sha":null,"homepage":"https://gooey.braebo.dev","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/braebo.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-07-07T17:03:15.000Z","updated_at":"2025-04-20T07:20:50.000Z","dependencies_parsed_at":"2024-08-10T01:24:25.843Z","dependency_job_id":"8f269a1c-7611-4081-902a-84288174293d","html_url":"https://github.com/braebo/gooey","commit_stats":null,"previous_names":["braebo/gooey"],"tags_count":19,"template":false,"template_full_name":null,"purl":"pkg:github/braebo/gooey","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/braebo%2Fgooey","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/braebo%2Fgooey/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/braebo%2Fgooey/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/braebo%2Fgooey/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/braebo","download_url":"https://codeload.github.com/braebo/gooey/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/braebo%2Fgooey/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259884417,"owners_count":22926440,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["draggable","gui","presets","typescript","window-manager"],"created_at":"2024-10-22T07:09:27.735Z","updated_at":"2025-06-14T21:03:13.898Z","avatar_url":"https://github.com/braebo.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./www/static/assets/gooey-meta@600.png\" alt=\"gooey\" width=\"300\"\u003e\u003c/img\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./www/static/assets/goos-dark.png\" alt=\"gooey\" width=\"500\"\u003e\u003c/img\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\nfloating gui library for the web\n\n\u003cbr\u003e\u003ca align=\"center\" href=\"https://gooey.braebo.dev\"\u003egooey.braebo.dev\u003c/a\u003e\n\n\u003c/p\u003e\n\n\u003cblockquote\u003e\n  \u003ch6\u003e🚧 Pre \u003ccode\u003ev1.0.0\u003c/code\u003e\u003c/h6\u003e\n  Changes are frequent and breaking 𛱠\n\u003c/blockquote\u003e\n\n\u003cbr\u003e\n\n\u003c!-- TOC --\u003e\n\n-   [Features](#features)\n-   [Getting Started](#getting-started)\n    -   [1. Install](#1-install)\n    -   [2. Create a new `Gooey`](#2-create-a-new-gooey)\n-   [Basics](#basics)\n-   [Inputs](#inputs)\n    -   [`add` vs `bind`](#add-vs-bind)\n    -   [`add`](#add)\n    -   [`bind`](#bind)\n-   [About](#about)\n-   [Roadmap](#roadmap)\n\u003c!-- /TOC --\u003e\n\n\u003cbr\u003e\n\n## Features\n\n\u003cp align=\"center\"\u003e\n\n\u0026nbsp; \u0026nbsp; \u0026nbsp;\u0026nbsp; Preset Manager \u0026nbsp;\u0026nbsp; · \u0026nbsp; Theme Manager \u0026nbsp;\u0026nbsp; · \u0026nbsp; Draggable / Resizable / Placeable \u0026nbsp;\u0026nbsp; · \u0026nbsp; Local Storage Integration \u0026nbsp;\u0026nbsp; · \u0026nbsp; Generators \u0026nbsp;\u0026nbsp; · \u0026nbsp; Reset Mechanism \u0026nbsp;\u0026nbsp; · \u0026nbsp; Undo/Redo History \u0026nbsp;\u0026nbsp; · \u0026nbsp; Flexible API \u0026nbsp;\u0026nbsp; · \u0026nbsp; Fully Typed \u0026nbsp;\u0026nbsp; · \u0026nbsp; Zero Dependencies\n\n\u003c/p\u003e\n\n\u003cbr\u003e\n\n## Getting Started\n\n### 1. Install\n\n```elixir\nnpm install gooey\n```\n\n```typescript\nimport { Gooey } from 'gooey'\n```\n\n\u003cdetails\u003e\u003csummary\u003eMore Install Methods\u003c/summary\u003e\n\u003cbr\u003e\n\u003c!-- Alternatives--\u003e\n\u003c!-- There are a few other ways to install `gooey`, most of which I recommend over NPM: --\u003e\n\n\u003c!-- \u003cbr\u003e --\u003e\n\n\u003cdetails\u003e\u003csummary\u003eJSR\u003c/summary\u003e\n\n\u003ca href=\"https://jsr.io\"\u003eJSR\u003c/a\u003e is a modern alternative to NPM\n\n```elixir\nnpx jsr add @braebo/gooey\n```\n\n```typescript\nimport { Gooey } from '@braebo/gooey'\n```\n\n\u003cbr\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003ePNPM\u003c/summary\u003e\n\n\u003ca href=\"https://pnpm.io\"\u003ePNPM\u003c/a\u003e is the recommended way to install `gooey`.\n\n```elixir\npnpm i -D gooey\n```\n\n```typescript\nimport { Gooey } from 'gooey'\n```\n\n\u003cbr\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eCDN\u003c/summary\u003e\n\n```html\n\u003cscript type=\"module\"\u003e\n\timport { Gooey } from 'https://esm.sh/gooey'\n\n\tconst gui = new Gooey()\n\u003c/script\u003e\n```\n\n\u003c/details\u003e\n\n\u003c/details\u003e\n\n\u003cbr\u003e\n\n### 2. Create a new `Gooey`\n\n```typescript\nconst gui = new Gooey()\n```\n\n\u003cbr\u003e\n\n## Basics\n\nUse [`add`](#add) to create a new [input](#inputs)\n\n```typescript\ngooey.add('hello', 'world')\n\ngooey.add('count', 1, { min: -1 })\n```\n\n\u003cbr\u003e\n\nUse [`addMany`](#addmany) to create multiple inputs at once:\n\n```typescript\ngooey.addMany({\n\tstuff: true,\n\tmore_stuff: {\n\t\tlike_colors: '#4aa7ff' as const,\n\t\tor_buttons: () =\u003e alert('thanks!'),\n\t},\n})\n```\n\n\u003cbr\u003e\n\n\u003c!-- events - `on` method --\u003e\n\nDo stuff [`on`](#on) change:\n\n```typescript\nconst greetingInput = gooey.add('greeting', 'hello')\n\ngreetingInput.on('change', console.log) // logs the text value when changed\n```\n\n\u003cdetails\u003e\u003csummary\u003e\u003cem\u003e\u003csup\u003ealternatives\u003c/sup\u003e\u003c/em\u003e\u003c/summary\u003e\n\nThe `onChange` option can also be used to set a callback that will be called when the value changes:\n\n```typescript\ngooey.add('title', 'change me', {\n\tonChange: v =\u003e (gooey.title = v),\n})\n```\n\nOr, you can chain stuff:\n\n```typescript\ngooey.add('title', 'change me').on('change', v =\u003e (gooey.title = v))\n```\n\n\u003c/details\u003e\n\n\u003cbr\u003e\n\n\u003c!-- bind - `bind` method --\u003e\n\nInstead of using [`add`](#add) with event callbacks, you can use [`bind`](#bind) to automatically sync an object's values with an input. For example:\n\n```typescript\nconst data = {\n\tsize: 12,\n\tcolor: '#4aa7ff' as const,\n}\n\ngooey.bind(data, 'size')\ngooey.bind(data, 'color')\n```\n\n\u003cbr\u003e\n\n\u003c!-- bindMany - `bindMany` method --\u003e\n\nBind to an entire object with [`bindMany`](#bindmany)\n\n```typescript\nconst data = {\n\twght: 100,\n\twdth: 75,\n}\n\ngooey.bindMany(data)\n```\n\n\u003c!-- todo - these details should move to the `bindMany` section --\u003e\n\n\u003c!-- \u003cdetails\u003e\u003csummary\u003e\u003csup\u003emore info\u003c/sup\u003e\u003c/summary\u003e\n\nYou can also pass options to `bindMany`:\n\n```typescript\nconst data = {\n    wght: 300,\n    wdth: 100,\n}\n\ngooey.bindMany(data, {\n    wght: { min: 100, max: 900 },\n    wdth: { min: 100, max: 130 },\n})\n```\n\nThe types here will be inferred for all that intellisense goodness.\n\n\u003c/details\u003e --\u003e\n\n\u003cbr\u003e\n\nCreate [folders](#folders) with `addFolder`\n\n```typescript\nconst outer = gooey.addFolder('outer')\n\nconst inner = outer.addFolder('inner')\n\ninner.add('say sike', () =\u003e outer.close(), {\n\ttext: 'sike',\n})\n```\n\n\u003c!-- todo - these details should move to the `Folder` section --\u003e\n\n\u003c!-- \u003cdetails\u003e\u003csummary\u003ePro tip \u0026nbsp;·\u0026nbsp; click folder headers to open / close them\u003c/summary\u003e\n\nThe `closed` state can be persisted in `localStorage`:\n\n```typescript\n// Persist all states\nconst gooey = new Gooey({ storage: true })\n\n// Only `closed` state\nconst gooey = new Gooey({\n    storage: {\n        closed: true,\n    },\n})\n```\n\n\u003c/details\u003e --\u003e\n\n\u003cbr\u003e\n\n## Inputs\n\n\u003cdetails\u003e\u003csummary\u003eInputs Table\u003c/summary\u003e\u003cbr\u003e\n\n| Status | Feature    | Primitive                      |\n| ------ | ---------- | ------------------------------ |\n| ✅     | Number     | `number`                       |\n| ✅     | Text       | `string`                       |\n| ✅     | Switch     | `boolean`                      |\n| ✅     | Select     | `Array\u003cany\u003e`                   |\n| ✅     | Button     | `{ text, onClick, ... }`       |\n| ✅     | ButtonGrid | `{ text, onClick, ... }[][]`   |\n| ✅     | Color      | `Color \\| ColorRepresentation` |\n| 🏗️     | Range      | `{ min, max }`                 |\n| 🏗️     | Vector2    | `{ x, y }`                     |\n| 🏗️     | Vector3    | `{ x, y, z }`                  |\n\n\u003c/details\u003e\n\n\u003cbr\u003e\n\n### `add` vs `bind`\n\nThere are two ways to create inputs; [`add`](#add) or [`bind`](#bind) _(along with [`addMany`](#addmany) / [`bindMany`](#bindmany) for multiple inputs)_. The return value will be the generated `Input` instance.\n\n-   [`add`](#add) inputs when you have no data and just want to generate some values and hook into change [events](#events). The value will be created and managed by Gooey.\n\n-   [`bind`](#bind) inputs when you have an existing object, and you want its value(s) to stay in sync with the generated input(s) automatically. This is useful for data that's integrated into a larger system, like reactive state in a web app or entities in a 3D scene graph.\n\n\u003cbr\u003e\n\n### `add`\n\nThe `Folder.add` method can be used to create any input.\n\n```typescript\ntype add = \u003cT\u003e(\n  title: string,\n  initialValue: T,\n  options?: InputOptions\u003cT\u003e\n): Input\u003cT\u003e\n```\n\nIt accepts a `title`, `initialValue`, and `options` object.\n\n\u003e [!TIP]\n\u003e Passing an empty string as the `title` will omit the title's `\u003cdiv\u003e` element, allowing the input to fill the entire width of the parent folder.\n\n\u003cbr\u003e\n\nThe type of input generated depends on the type of the `initialValue` argument. For example, passing a `string` results in an `InputText` instance, while passing a `number` results in an `InputNumber` instance.\n\n```typescript\ngooey.add('my text', 'foo') // string -\u003e InputText\n\ngooey.add('my number', 1) // number -\u003e InputNumber\n```\n\n\u003cdetails\u003e\u003csummary\u003eAll \u003ccode\u003eadd\u003c/code\u003e overloads\u003c/summary\u003e\n\nSimple examples of each type of input that can be created with the `add` method _(with empty strings in the `title` arguments for simplicity)_:\n\n```typescript\n// InputText\ngooey.add('', 'foo') // string\n\n// InputNumber\ngooey.add('', 1) // number\n\n// InputColor\ngooey.add('', '#4aa7ff') // ColorValue\n\n// InputSelect\ngooey.add('', ['foo', 'bar']) // any[]\n\n// InputButton\ngooey.add('', () =\u003e alert('hi')) // () =\u003e void\n\n// InputSwitch\ngooey.add('', true) // boolean\n\n// InputButtonGrid\ngooey.add('', [\n\t[\n\t\t{ text: 'foo', onClick: () =\u003e alert('foo') },\n\t\t{ text: 'bar', onClick: () =\u003e alert('bar') },\n\t],\n\t[\n\t\t{ text: 'baz', onClick: () =\u003e alert('baz') },\n\t\t{ text: 'qux', onClick: () =\u003e alert('qux') },\n\t],\n]) // (() =\u003e void)[][]\n```\n\n\u003c/details\u003e\n\n\u003cbr\u003e\n\nThe type of the `options` object in the third argument will change depending on the type of input, for example:\n\n```typescript\nconst countInput = gooey.add('count', 1, {\n\tmin: -1,\n\tmax: 10,\n\tstep: 0.1,\n})\n```\n\nBecause the initial value _(`1`)_ is a **number**, gooey infers the options in the third argument as `NumberInputOptions` — which is why it accepts `min`, `max`, and `step`.\n\nIf we pass a **string** instead, it'll infer `TextInputOptions`:\n\n```typescript\nconst textInput = gooey.add('greeting', 'hello', {\n\tmaxLength: 10,\n})\n```\n\nThis should get you some nice, dynamic intellisense. However, you can always fall back to the more specific adders _(like `addNumber`, `addColor`, etc.)_ if need be.\n\n### `addMany`\n\nThe `Folder.addMany` method can be used to create multiple inputs at once.\n\n```typescript\ntype addMany = \u003cT\u003e(\n  target: T,\n  options?: Record\u003ckeyof T, InputOptions\u003cT\u003e\u003e \u0026 {\n    exclude?: Array\u003ckeyof T\u003e\n    include?: Array\u003ckeyof T\u003e\n  },\n) =\u003e {\n  folders: Folder[];\n  inputs: Input\u003cT\u003e[];\n}\n```\n\nIt takes in any object, and generates a set of inputs based on the object's keys and values.\n\nNested objects will result in child folders being created.\n\nOptions can be passed to the second argument to customize the inputs being generated, and/or to include/exclude specific keys from generation.\n\nWhile the simplified version of the type signature for `addMany` above might seem a bit complex, it's actually quite simple in practice! Let's break it down:\n\nThe `addMany` method takes two arguments:\n\n1. `target`: The object to create inputs from.\n2. `options`: Options to customize the inputs generated, as well as `include` and `exclude` arrays to omit certain keys.\n\nIt returns an object with two properties:\n\n1. `folders`: An array of `Folder` instances created from the object's nested objects, if any.\n2. `inputs`: An array of `Input` instances created from the object's primitive values.\n\nLet's look at an example to see how this works in practice.\n\n```typescript\nconst {inputs, folders} = gooey.addMany({\n  myNumber: 5,\n  myFolder: {\n    myColor: '#4aa7ff',\n  }\n})\n```\nThis will result in an `InputNumber`, and a `Folder` titled `myFolder` containing an `InputColor`.\n\n```typescript\ninputs.myNumber  // -\u003e InputNumber\ninputs.myColor   // -\u003e InputColor\n\nfolders.myFolder // -\u003e Folder\n```\n\nSuppose we want to configure the `min` and `max` options for `myNumber`.  To do this, we can specify them in the second argument:\n\n```typescript\nconst { inputs, folders }  = gooey.addMany({\n  myNumber: 5,\n  myFolder: {\n    myColor: '#4aa7ff',\n  }\n}, {\n  myNumber: {\n    min: 0,\n    max: 10,\n  }\n})\n```\n\nAnd that's it!  If all goes well, you should get strong intellisense for all available options in the second argument.  If you don't, please file an issue!\n\nSometimes, relying on inference won't be enough, and you'll need an escape-hatch to get the exact inputs you want.  In that case, you can `exclude` a key from generation, and create it manually with a more specific adder:\n\n```typescript\nconst { inputs, folders }  = gooey.addMany({\n  myNumber: 5,\n  myFolder: {\n    myColor: 'hsl(200, 100%, 50%)', // -\u003e InputText (wrong! let's exclude it)\n  }\n}, {\n  exclude: ['myColor'],\n})\n\n// ...and now we can add it manually:\ngooey.addColor('myColor', 'hsl(200, 100%, 50%)', {\n  // and customize it a bit:\n  mode: 'hsl'\n})\n```\n\n### `bind`\n\n```typescript\ntype bind = \u003cT\u003e(\n  target: T,\n  key: keyof T,\n  options?: InputOptions\u003cT\u003e\n): Input\u003cT\u003e\n```\n\nThe `Folder.bind` method can be used to create an input that is bound to a key on a target object. When an input created with `bind` is changed, the target object's value for the given key will be updated automatically.\n\n```typescript\nconst data = {\n\tsize: 12,\n\tcolor: '#4aa7ff' as const,\n}\n\ngooey.bind(data, 'size') // -\u003e InputNumber\ngooey.bind(data, 'color') // -\u003e InputColor\n```\n\n\u003cbr\u003e\n\n## About\n\nI built this to scratch an itch, and to pave the way for more advanced features related to WebGL / WebAudio / audio-reactive 3D in the future.\n\n[tweakpane](https://github.com/cocopon/tweakpane) was the main inspiration for this project. I recommend it over `gooey` -- it's a more lightweight solution with more features and an awesome, highly active developer!\n\nOther, similar projects:\n\n-   [lil-gui](https://github.com/georgealways/lil-gui)\n-   [dat.gui](https://github.com/dataarts/dat.gui)\n\n\u003cbr\u003e\n\n## Roadmap\n\n-   [ ] Graph / Monitor Input\n-   [ ] LFO / Envelope Manager\n-   [ ] Bezier Curve Editor Input\n-   [ ] Plugins\n-   [ ] Framework Wrappers (svelte 5 runes \"just work\", but other frameworks need wrappers)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbraebo%2Fgooey","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbraebo%2Fgooey","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbraebo%2Fgooey/lists"}