├── .babelrc ├── .editorconfig ├── .eslintrc ├── .github └── workflows │ ├── npm-publish.yml │ └── test.yml ├── .gitignore ├── .meta-files └── images │ └── Hero-Screenshot.png ├── .nvmrc ├── changelog.md ├── contributing.md ├── docs ├── Guify.png └── api.md ├── example └── index.html ├── lib ├── guify.js ├── guify.js.map ├── guify.min.js ├── guify.min.js.LICENSE.txt └── guify.min.js.map ├── license.md ├── package-lock.json ├── package.json ├── postcss.config.js ├── readme.md ├── src ├── component-manager.js ├── components │ ├── component-base.js │ ├── internal │ │ ├── menu-bar.css │ │ ├── menu-bar.js │ │ ├── panel.css │ │ ├── panel.js │ │ ├── toast-area.css │ │ └── toast-area.js │ ├── partials │ │ ├── container.css │ │ ├── container.js │ │ ├── header.js │ │ ├── label.css │ │ ├── label.js │ │ ├── value.css │ │ └── value.js │ ├── public │ │ ├── button.css │ │ ├── button.js │ │ ├── checkbox.css │ │ ├── checkbox.js │ │ ├── color.css │ │ ├── color.js │ │ ├── display.css │ │ ├── display.js │ │ ├── file.css │ │ ├── file.js │ │ ├── folder.css │ │ ├── folder.js │ │ ├── interval.css │ │ ├── interval.js │ │ ├── range.css │ │ ├── range.js │ │ ├── select.css │ │ ├── select.js │ │ ├── text.css │ │ ├── text.js │ │ ├── title.css │ │ └── title.js │ └── variables.css ├── gui.css ├── gui.js ├── guify.js ├── theme.js ├── themes.js └── utils │ └── math-utils.js ├── test └── library.spec.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": [] 4 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = LF 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended" 9 | ], 10 | "ignorePatterns": [ 11 | "webpack.config.js", 12 | "lib", 13 | "test/**/*.spec.js" 14 | ], 15 | "rules": { 16 | "semi": ["error", "always"], 17 | "quotes": ["error", "double"], 18 | "no-unused-vars": 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: 14 18 | cache: 'npm' 19 | - run: npm ci 20 | - run: npm test 21 | 22 | publish-npm: 23 | needs: build 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - uses: actions/setup-node@v2 28 | with: 29 | node-version: 14 30 | cache: 'npm' 31 | registry-url: https://registry.npmjs.org/ 32 | - run: npm ci 33 | - run: npm publish 34 | env: 35 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 36 | 37 | # publish-gpr: 38 | # needs: [build, publish-npm] 39 | # runs-on: ubuntu-latest 40 | # permissions: 41 | # contents: read 42 | # packages: write 43 | # steps: 44 | # - uses: actions/checkout@v2 45 | # - uses: actions/setup-node@v2 46 | # with: 47 | # node-version: 14 48 | # cache: 'npm' 49 | # registry-url: https://npm.pkg.github.com/ 50 | # - run: npm ci 51 | # - run: npm publish 52 | # env: 53 | # NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 54 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x, 16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | todo.md 3 | notes.md 4 | 5 | # Created by https://www.gitignore.io/api/osx,node,linux,windows 6 | 7 | ### Linux ### 8 | *~ 9 | 10 | # temporary files which can be created if a process still has a handle open of a deleted file 11 | .fuse_hidden* 12 | 13 | # KDE directory preferences 14 | .directory 15 | 16 | # Linux trash folder which might appear on any partition or disk 17 | .Trash-* 18 | 19 | # .nfs files are created when an open file is removed but is still being accessed 20 | .nfs* 21 | 22 | ### Node ### 23 | # Logs 24 | logs 25 | *.log 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # Runtime data 31 | pids 32 | *.pid 33 | *.seed 34 | *.pid.lock 35 | 36 | # Directory for instrumented libs generated by jscoverage/JSCover 37 | lib-cov 38 | 39 | # Coverage directory used by tools like istanbul 40 | coverage 41 | 42 | # nyc test coverage 43 | .nyc_output 44 | 45 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 46 | .grunt 47 | 48 | # Bower dependency directory (https://bower.io/) 49 | bower_components 50 | 51 | # node-waf configuration 52 | .lock-wscript 53 | 54 | # Compiled binary addons (http://nodejs.org/api/addons.html) 55 | build/Release 56 | 57 | # Dependency directories 58 | node_modules/ 59 | jspm_packages/ 60 | 61 | # Typescript v1 declaration files 62 | typings/ 63 | 64 | # Optional npm cache directory 65 | .npm 66 | 67 | # Optional eslint cache 68 | .eslintcache 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variables file 80 | .env 81 | 82 | 83 | ### OSX ### 84 | *.DS_Store 85 | .AppleDouble 86 | .LSOverride 87 | 88 | # Icon must end with two \r 89 | Icon 90 | 91 | # Thumbnails 92 | ._* 93 | 94 | # Files that might appear in the root of a volume 95 | .DocumentRevisions-V100 96 | .fseventsd 97 | .Spotlight-V100 98 | .TemporaryItems 99 | .Trashes 100 | .VolumeIcon.icns 101 | .com.apple.timemachine.donotpresent 102 | 103 | # Directories potentially created on remote AFP share 104 | .AppleDB 105 | .AppleDesktop 106 | Network Trash Folder 107 | Temporary Items 108 | .apdisk 109 | 110 | ### Windows ### 111 | # Windows thumbnail cache files 112 | Thumbs.db 113 | ehthumbs.db 114 | ehthumbs_vista.db 115 | 116 | # Folder config file 117 | Desktop.ini 118 | 119 | # Recycle Bin used on file shares 120 | $RECYCLE.BIN/ 121 | 122 | # Windows Installer files 123 | *.cab 124 | *.msi 125 | *.msm 126 | *.msp 127 | 128 | # Windows shortcuts 129 | *.lnk 130 | 131 | # End of https://www.gitignore.io/api/osx,node,linux,windows 132 | -------------------------------------------------------------------------------- /.meta-files/images/Hero-Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colejd/guify/4cafddd568859fd7c33faaf9550c5e6ab3fe8e13/.meta-files/images/Hero-Screenshot.png -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v6.10 2 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ## 0.15.1 2 | 3 | - Fixed a bug preventing folders from being removed with `Remove()`. 4 | - There was an issue introduced by 0.15.0 where the guify-container covers all content and has a higher z-index than the content underneath, eating all touch/click events. To fix this, the z-index of guify-container is now unset, and the sub-elements are given the z-index that guify-container used to get (9999). 5 | 6 | ## 0.15.0 7 | 8 | - POTENTIALLY BREAKING CHANGE: Modified the way the GUI elements are constructed internally. If you're modifying the internals in your CSS, make sure everything looks right! 9 | - Made menu bar visible in fullscreen 10 | - If `panelMode` is `outer`, the menu will become `inner` when fullscreen 11 | - Introduced `.guify-fullscreen` CSS class that attaches to the root when fullscreen is enabled 12 | - Fixed up `barMode = "none"` behavior to match docs 13 | - Added `panelOverflowBehavior` parameter to GUI opts, which lets you make the panel scrollable if it grows beyond the edge of the container. 14 | - Fixed brief display of incorrect value when initializing `range` and `display` 15 | - Added a bit of top margin for `title` components 16 | - Fixed styling issues on Safari iOS for `text`, `range`, and `checkbox` 17 | - Fixed incorrect font use on Safari iOS. 18 | - Added an `inputFontFamily` param to theme objects, allowing a secondary font just for input fields 19 | - If you provide your own font URL to the theme object, the default `"Hack"` font won't be downloaded 20 | - Made `range` and `interval` components respect `precision` more closely. `precision` now affects the value itself, meaning the value and its display will always match. 21 | - Fixed a bug in `interval` components with a `log` scale, wherein setting the value text would cause the wrong value to be used 22 | 23 | ## 0.14.3 24 | 25 | - Fixed vertical alignment of arrow in `folder` component 26 | 27 | ## 0.14.2 28 | 29 | - Fixed extra padding below `range` and `color` components 30 | - Fixed vertical alignment of `title` component 31 | 32 | ## 0.14.1 33 | 34 | - Fixed incorrect `interval` height and background 35 | - Made component height `2rem` by default 36 | - Should fix component height issues on some pages 37 | - Reduced line height for `display` component 38 | 39 | ## 0.14.0 40 | 41 | - Allow setting input listening mode on `text` components using a new `listenMode` option. New values are `"input"` (default) and `"change"`. 42 | - Rewrote `interval` component, and added the new features from the `range` improvements in 0.13.0. 43 | - `steps` has been removed for logarithmic sliders. 44 | - You can now specify `precision` for the readouts. 45 | - Added the ability to enable/disable components with `SetEnabled(Bool)`. 46 | - I added new theme elements `"colorTextDisabled"` and `"colorComponentBackgroundDisabled"` to support this. If you're using a custom theme, make sure you add values for these! 47 | - This involved totally rewriting the way styles are added to components internally. This shouldn't cause any issues externally, but if you encounter anything, please file an issue! 48 | - Updated dependencies 49 | - Redid NPM build scripts. See readme for updated commands 50 | - Fixed checkbox bug reported in #6 51 | - Checkbox can now be toggled by clicking anywhere in the row 52 | - Made it possible to have nested folders with identical names 53 | 54 | Thank you to @indivisualvj for your PR (#20)! 55 | 56 | ## 0.13.1 57 | 58 | - Fix missing upload artifacts 59 | 60 | ## 0.13.0 61 | 62 | - Rewrote logic for `range` component. 63 | - `steps` has been removed for logarithmic sliders. 64 | - Updated dependencies. 65 | 66 | ## 0.12.0 67 | 68 | - Added Interval control type (Thank you @ccrisrober!) 69 | - Step for Range and Interval controls is now 0.01 if not specified. Fixes weirdness with values changing in unexpected ways when typing in a new value. 70 | 71 | #### 0.11.1 72 | 73 | - Improved fullscreen API; Safari is now supported 74 | 75 | ## 0.11.0 76 | 77 | - Addded a fullscreen button 78 | - Updated NPM dependencies to fix vulnerabilities 79 | 80 | #### 0.10.1 81 | 82 | - Fix issue where checkboxes cannot be focused with tab 83 | 84 | ## 0.10.0 85 | 86 | - **Breaking change:** Export syntax has been simplified. Instead of `new guify.GUI(...)`, 87 | the call is now `new guify(...)`. 88 | - Styling: 89 | - Components now grow to fit the `componentHeight` property of the current theme 90 | - Set z-index on entire plugin so it overlaps everything else 91 | - Styles moved from component files to style files where appropriate 92 | - Fix Range vertical spacing 93 | - Improve Checkbox style coverage (should fix issues with iOS) 94 | - Simplify document model for container 95 | 96 | #### 0.9.2 97 | 98 | - Fix Range slider vertical offset in Firefox 99 | 100 | #### 0.9.1 101 | 102 | - Adjusted spacing of Range and Color's subcomponents 103 | - Improved example layout 104 | - Make Color's Value subcomponent `readonly` instead of `disabled` 105 | 106 | ## 0.9.0 107 | 108 | - Added `"panelMode"` initialization option 109 | - Allow user input in Range component value boxes 110 | - Fix button text vertical alignment 111 | - Force Value component font size to be the same across all themes 112 | 113 | #### 0.8.1 114 | 115 | - Fix Toast text coloring 116 | - Added `open` opt. Set to true to have the panel forced open on startup. 117 | 118 | ## 0.8.0 119 | 120 | - Updated Menu Bar look 121 | - Removed `menuBarContentHeight` property of themes 122 | - Added new button for opening/closing a Panel when `barMode` is `"none"` 123 | - Refactored theming code 124 | - Added new parameters for themes 125 | - Added new theme preset (`"yorha"`, based on https://metakirby5.github.io/yorha/) 126 | - File and Folder components now release focus if using a mouse to interact 127 | - Fix Range component not highlighting when focused in Firefox 128 | - Removed seam between the Panel and the container's edge on Chrome 129 | 130 | 131 | #### 0.7.2 132 | 133 | - Actually fixed `"above"` barMode 134 | 135 | #### 0.7.1 136 | 137 | - Fixed `"above"` barMode 138 | 139 | ## 0.7.0 140 | 141 | - Menu Bar, Panel and Toast Area have been moved into their own classes and files 142 | - Now using ES6-style imports in all source files 143 | - Added `"none"` option to `opts.barMode` (removed `opts.useMenuBar`) 144 | - Improve styling resistance against Bootstrap 145 | - Massive rewrite of styling: 146 | - Using CSJS instead of Sass so we can load CSS with dynamic variables 147 | - **Themes now work** 148 | 149 | ## 0.6.0 150 | 151 | - Toast notifications now have `aria-live="polite"` [accessibility] 152 | - Components with bound variables will now update themselves only if the bound value has changed 153 | - This is still checked every frame, which is something I'd like to avoid. I'm looking into it. 154 | - Made component polling rate part an option in GUI `opts` 155 | - Text and Range components will no longer update from their bound variables while focused 156 | - Styling update: 157 | - Component elements can now grow vertically 158 | - Range now defocuses after mouseup when using the mouse (stays focused if using a keyboard) [accessibility] 159 | - Checkbox now shows on/off styles when focused using a keyboard [accessibility] 160 | - Button and File components now give visual feedback when clicked 161 | - File component shows an outline when a file is dragged onto it 162 | - Select component now highlights on mouseover or focus 163 | - Fix focus highlighting issues on Firefox 164 | - Add font support to themes [tentative] 165 | - Changed Title component look 166 | - Display component text is now selectable. 167 | - Adjusted margin spacing for folders and titles 168 | 169 | 170 | ## 0.5.0 171 | 172 | - Add File component 173 | - Add Display component 174 | 175 | 176 | ## 0.4.0 177 | 178 | - Accessibility update: made components keyboard-accessible 179 | - Color component still needs work 180 | - Added folder component 181 | - Made `Register()` a method that can accept multiple options objects to instantiate many at once 182 | - Components now update themselves from bound variables 183 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | All work on this library is done against the `develop` branch, which gets merged into `main` when a new release is ready. Accordingly, if you'd like to contribute to Guify, please work off of the `develop` branch. 4 | 5 | This isn't strictly necessary, but please make sure you're not introducing any new ESLint warnings. If you're on VS Code, you can install the ESLint plugin to make this easier, or you can run it from the command line with `npm run lint`. -------------------------------------------------------------------------------- /docs/Guify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colejd/guify/4cafddd568859fd7c33faaf9550c5e6ab3fe8e13/docs/Guify.png -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | All options are optional unless marked as required. 3 | 4 | ## GUI 5 | 6 | Creates and maintains the entire GUI. If you want to show or hide the menu bar or panel, use `SetVisible(show)` or `ToggleVisible()` on `menubar` or `panel`. 7 | 8 | ### `constructor(opts)` 9 | Creates the GUI using the `opts` object for configuration. 10 | 11 | `opts` may have the following properties: 12 | - `title` (String): The name used on the menu bar / panel. 13 | - `theme` (String or Object, default=`"dark"`): The name of the theme to be used, or an object matching one of the themes in `themes.js` if you want to create your own. 14 | - Values: `"light"`, `"dark"`, `"yorha"`, `custom theme object` 15 | - If you use a custom theme object, see `theme.js` for the variables you can set. 16 | - `root` (Element, default=`document.body`): The HTML element that is used as the parent of the created menu and panel. 17 | - `width` (String, default=`"300"`): The width of the panel. You can use any CSS-compatible value here, so you can use `"30%"` or `"20em"`, for example. 18 | - `align` (String, default=`"right"`): Aligns the panel to the left or right side of the root. 19 | - Values: `"left"`, `"right"` 20 | - `barMode` (String, default=`"offset"`): Changes the way the layout bar is inserted into the root. 21 | - Values: 22 | - `"none"`: No menu bar is created, and the panel will always show. 23 | - `"overlay"`: The menu bar is fixed to the top of the root, overlapping content beneath it. 24 | - `"above"`: The menu bar is fixed above the root. Does not alter layout within root. 25 | - In this mode, the menu bar can overlap content just above the root. If you don't want this, you can either use the `"offset"` mode, or set `margin-top: var(--size-menu-bar-height)`. 26 | - `"offset"`: Similar to `"above"`, but some `"margin-top"` is added to the root to compensate for the menu bar's height. 27 | - I've tried to cover a variety of use cases here. If yours isn't covered, you can use `var(--size-menu-bar-height)` in your CSS to offset things yourself. 28 | - `panelMode` (String, default=`"inner"`): Changes the way the panel is anchored relative to the container. 29 | - Values: 30 | - `"inner"`: The panel shows inside of the container. 31 | - `"outer"`: The panel shows outside the container, positioned along whichever side you specified with `align`. 32 | - If you want to put the panel anywhere, use `"inner"` and adjust the CSS however you'd like. 33 | - `panelOverflowBehavior` (String, default=`"scroll"`): Changes the way the panel behaves when its contents exceed the height of the container. 34 | - Values: 35 | - `"scroll"`: The contents will be scrollable. 36 | - `"overflow"`: The panel will grow beyond the edge of the container. 37 | - `opacity` (float, default=`1.0`): Opacity value for the panel. 38 | - `pollRateMS` (int, default=`100`): The rate in milliseconds at which the components will be refreshed from their bound variables. 39 | - `open` (bool, default=`false`): If true, the panel will be forced open at startup. 40 | 41 | 42 | ### `Toast(message, stayMS, transitionMS)` 43 | Displays a toast-style message on screen. `stayMS` and `transitionMS` are optional values that you can use to control the duration and removal animation time of the notification. 44 | 45 | ### `Register(opts, applyToAll)` 46 | Creates a new component in the panel based on `opts`. You can provide one `opts` object or an array if you want to create many components at once. 47 | Returns the component. 48 | 49 | ### `Remove(component)` 50 | Removes the specified component. 51 | 52 | All properties of `applyToAll` will be applied to each opts object. 53 | 54 | ## Components 55 | 56 | Components have a few shared methods you may call after initialization. 57 | 58 | - `SetEnabled(enabled)`: Sets the component enabled/disabled style. 59 | - `Remove()`: Removes the component from the GUI. 60 | 61 | ### `opts` 62 | The properties in this object determine the type and behavior of the created component. Pass this into `Register(opts)`. 63 | 64 | The common properties are: 65 | 66 | - `type` (String, required): The component type that will be created. Can be `"button"`, `"checkbox"`, `"color"`, `"range"`, `"select"`, `"text"`, and `"title"` 67 | - `label` (String): The text label that appears next to the component. 68 | - `initial` (Object): The initial value of the component. If you don't specify this, it will be copied from the bound value if there is one, or otherwise initialized to the variable type's default value. 69 | - `onChange` (callback): Fired every time the value governed by the component changes, with a single argument holding the value. 70 | - `onInitialize` (callback): Fired when the component is initialized. 71 | - `object` (Object): The object holding the property you want the component to be bound to. 72 | - `property` (String): The name of the property in `object` that you want the component to be bound to. `object[property]` and the value of the component will be bound (updating one will change the other). 73 | - `folder` (String): The label of the folder to put the component into. If none is specified it'll just go in the panel at the root level. 74 | - `enabled` (Bool): Whether the component starts out enabled or not (only works for interactive components). This can be modified at runtime with `component.SetEnabled(Bool)`. 75 | 76 | Some component types have their own options. These will be specified for each component listed below. 77 | 78 | ### Text 79 | `type: 'text'` 80 | 81 | Shows an editable text box. 82 | 83 | Special options: 84 | - `listenMode` (String, default=`"input"`): Corresponds to the string you'd pass to `addEventListener()` on a vanilla text field. Can be either `"input"` or `"change"`. 85 | - `"input"` makes it so that every keystroke sends an event. 86 | - `"change"` makes it so that an event is only sent when the field loses focus or you press Enter. 87 | 88 | ### Button 89 | `type: 'button'` 90 | 91 | Represents a button. 92 | 93 | Special options: 94 | - `action` (callback): Called when the button is clicked. 95 | 96 | ### Checkbox 97 | `type: 'checkbox'` 98 | 99 | Represents true/false. 100 | 101 | ### Color 102 | `type: 'color'` 103 | 104 | Represents a color. Can show RGB or hex colors. 105 | 106 | Special options: 107 | - `format` (String): Can be either `"rgb"` or `"hex"`. 108 | 109 | ### Display 110 | `type: 'display'` 111 | 112 | Displays the bound value. 113 | 114 | ### File 115 | `type: 'file'` 116 | 117 | Button / drop area for file selection. 118 | 119 | Special options: 120 | - `fileReadFunc` (String): The name of the method you want the FileReader inside this class to read files with. See the [FileReader docs](https://developer.mozilla.org/en-US/docs/Web/API/FileReader) for more about what these methods do. 121 | - Values: `"readAsDataURL"` (default), `"readAsArrayBuffer"`, `"readAsBinaryString"`, `"readAsText"` 122 | 123 | ### Range 124 | `type: 'range'` 125 | 126 | Shows a slider representing a numerical value. 127 | 128 | Special options: 129 | - `min` (int): The smallest possible value on the slider. 130 | - `max` (int): The largest possible value on the slider. 131 | - `precision` (int, default=`3`): The maximum number of digits displayed for the value if it's a decimal. 132 | - `step` (int, default=`0.01` [see notes]): The amount that is incremented by each movement of the slider. Only effective when `"scale = linear"`. 133 | - If the `precision` is set, then the `step` will by default be the smallest value possible given the precision. For example, if `precision = 3`, then `step = 0.01`, or if `precision = 5`, then `step = 0.0001`. 134 | - `scale` (String): Specifies the scaling behavior of the slider. 135 | - Values: `"linear"`, `"log"` 136 | 137 | ### Interval 138 | `type: 'interval'` 139 | 140 | Shows an adjustable two-handle slider representing an interval. 141 | - `min` (int): The smallest possible value on the slider. 142 | - `max` (int): The largest possible value on the slider. 143 | - `precision` (int, default=`3`): The maximum number of digits displayed for the value if it's a decimal. 144 | - `step` (int, default=`0.01`): The amount that is incremented by each movement of the slider. 145 | - If the `precision` is set, then the `step` will by default be the smallest value possible given the precision. For example, if `precision = 3`, then `step = 0.01`, or if `precision = 5`, then `step = 0.0001`. 146 | - `scale` (String): Specifies the scaling behavior of the slider. 147 | - Values: `"linear"`, `"log"` 148 | 149 | ### Select 150 | `type: 'select'` 151 | 152 | Shows a dropdown with the specified options. 153 | 154 | Special options: 155 | - `options` (Array(String)): A list of strings representing the different selectable options. 156 | 157 | ### Folder 158 | `type: 'folder'` 159 | 160 | An expanding/collapsing area that you can put other components into. To do this, use `folder: 'folderLabel'` as an option of another component, where `folderLabel` is the label of a folder. 161 | 162 | Special options: 163 | - `open` (bool, default=`true`): Show or hide the folder by default 164 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Guify Example 5 | 6 | 7 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 |
56 | [content] 57 |
58 |
59 |
60 | 61 | 317 | -------------------------------------------------------------------------------- /lib/guify.min.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * EventEmitter v5.2.8 - git.io/ee 3 | * Unlicense - http://unlicense.org/ 4 | * Oliver Caldwell - https://oli.me.uk/ 5 | * @preserve 6 | */ 7 | 8 | /*! 9 | * screenfull 10 | * v5.0.0 - 2019-09-09 11 | * (c) Sindre Sorhus; MIT License 12 | */ 13 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Jonathan Cole (jons.website) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------- 10 | 11 | **Portions of this code are adapted from https://github.com/freeman-lab/control-panel, which is licensed and copyrighted as follows:** 12 | 13 | The MIT License (MIT) 14 | 15 | Copyright (c) 2016 Jeremy Freeman 16 | 17 | Permission is hereby granted, free of charge, to any person obtaining a copy 18 | of this software and associated documentation files (the "Software"), to deal 19 | in the Software without restriction, including without limitation the rights 20 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 21 | copies of the Software, and to permit persons to whom the Software is 22 | furnished to do so, subject to the following conditions: 23 | 24 | The above copyright notice and this permission notice shall be included in all 25 | copies or substantial portions of the Software. 26 | 27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 32 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 33 | SOFTWARE. 34 | 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": "--------------- Script Info --------------- ", 3 | "name": "guify", 4 | "author": "Jonathan Cole ", 5 | "version": "0.15.1", 6 | "description": "A simple GUI for inspecting and changing JS variables", 7 | "keywords": [ 8 | "gui", 9 | "ui", 10 | "inspect", 11 | "inspector", 12 | "bind", 13 | "binding", 14 | "project", 15 | "creative coding", 16 | "p5", 17 | "three" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/colejd/guify.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/colejd/guify/issues" 25 | }, 26 | "homepage": "https://github.com/colejd/guify#readme", 27 | "license": "MIT", 28 | "main": "lib/guify.min.js", 29 | "scripts": { 30 | "build:prod": "webpack --mode=production", 31 | "build:dev": "webpack --mode=development --progress", 32 | "buildall": "npm-run-all build:prod build:dev", 33 | "build:dev:watch": "webpack --mode=development --progress --watch", 34 | "serve": "webpack serve --mode=development", 35 | "develop": "npm-run-all --parallel build:dev:watch serve", 36 | "test": "mocha --require @babel/register --colors ./test/*.spec.js", 37 | "prepublish": "npm run-script buildall", 38 | "ci": "npm run-script buildall", 39 | "lint": "eslint ." 40 | }, 41 | "devDependencies": { 42 | "@babel/cli": "^7.16.0", 43 | "@babel/core": "^7.16.0", 44 | "@babel/eslint-parser": "^7.16.3", 45 | "@babel/preset-env": "^7.16.0", 46 | "@babel/register": "^7.16.0", 47 | "babel-loader": "^8.2.3", 48 | "chai": "^4.3.4", 49 | "css-loader": "^6.5.1", 50 | "eslint": "^8.2.0", 51 | "eslint-webpack-plugin": "^3.1.0", 52 | "mocha": "^9.1.3", 53 | "npm-run-all": "^4.1.5", 54 | "postcss": "^8.3.11", 55 | "postcss-loader": "^6.2.0", 56 | "postcss-preset-env": "^7.0.0", 57 | "style-loader": "^3.3.1", 58 | "webpack": "^5.63.0", 59 | "webpack-cli": "^4.9.1", 60 | "webpack-dev-server": "^4.4.0" 61 | }, 62 | "dependencies": { 63 | "dom-css": "^2.1.0", 64 | "is-numeric": "0.0.5", 65 | "is-string": "^1.0.4", 66 | "param-case": "^2.1.1", 67 | "screenfull": "^5.0.0", 68 | "simple-color-picker": "^1.0.5", 69 | "tinycolor2": "^1.4.1", 70 | "uuid": "^3.4.0", 71 | "wolfy87-eventemitter": "^5.2.2" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | [ 4 | "postcss-preset-env", 5 | { 6 | stage: 3, 7 | features: { 8 | "nesting-rules": true, 9 | } 10 | }, 11 | ], 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # guify 2 | 3 |

4 | 5 |

6 |

7 | npm version 8 |

9 | 10 |

11 | Demo 12 | | 13 | Docs 14 |

15 | 16 | Guify is a runtime JS library that gives you a simple way to build a GUI for your JS projects. It pairs very well with [three.js](https://threejs.org/) and [p5.js](https://p5js.org/). Consider it an opinionated take on [dat.GUI](https://github.com/dataarts/dat.gui). 17 | 18 | Here are the big features: 19 | 20 | * Bind any UI component to any variable. Guify supports arbitrary text inputs, colors, ranges, file inputs, booleans, and more. 21 | * Guify is easy to graft onto any page and integrate with your existing JS code. Just point your components at the variables you already have: 22 | ```js 23 | var someVariable = 0; 24 | guify.Register([{ 25 | { 26 | type: 'range', 27 | object: this, property: 'someProperty', 28 | label: 'Some Property', 29 | min: 0, max: 20, step: 1 30 | }, 31 | }]) 32 | ``` 33 | * Give it that "web app" look with an optional header bar and easy toast notifications. 34 | * Style it however you'd like. You can use one of three built-in themes, or build your own to get exactly the look you want. 35 | 36 | --- 37 | 38 | ## Installation 39 | 40 | Below are some common ways to integrate Guify with your setup. 41 | 42 | ### Quick Start (Browser) 43 | 44 | To integrate on an existing page, you can use the transpiled version in [`/lib`](/lib), either by including it with your files or using a CDN: 45 | 46 | ```html 47 | 48 | ``` 49 | 50 | This adds a `guify` function at the global level, which you can use to construct the GUI. For example: 51 | 52 | ```html 53 | 54 | 55 | 59 | ``` 60 | 61 | See the Usage guide below for more details. [example.html](/example/index.html) also demonstrates this pattern. 62 | 63 | ### Quick Start (NPM) 64 | 65 | First, install with NPM: `npm install --save guify` 66 | 67 | Then you can import using either `require` or `import` depending on your preference: 68 | ```js 69 | // ES6 70 | import guify from 'guify' 71 | 72 | // Require 73 | let guify = require('guify'); 74 | ``` 75 | 76 | Then you can make a quick GUI this way: 77 | ```js 78 | var gui = new guify({ ... }); 79 | gui.Register([ ... ]); 80 | ``` 81 | 82 | See the Usage guide below for more details. 83 | 84 | ### Quick Start (React) 85 | 86 | Check out the unofficial [React port](https://github.com/dbismut/react-guify). 87 | 88 | --- 89 | 90 | ## Usage 91 | 92 | Once you have Guify available to construct in your project, make a `guify` instance: 93 | 94 | ```js 95 | var gui = new guify({ 96 | title: "Some Title", 97 | }); 98 | ``` 99 | 100 | The various controls in Guify are called "components". You can feed component definitions to Guify as follows: 101 | 102 | ```js 103 | gui.Register([ 104 | { // A slider representing a value between 0 and 20 105 | type: 'range', label: 'Range', 106 | min: 0, max: 20, step: 1, 107 | onChange: (value) => { 108 | console.log(value); 109 | } 110 | }, 111 | { 112 | type: 'button', label: 'Button', 113 | action: () => { 114 | console.log('Button clicked!'); 115 | } 116 | }, 117 | { 118 | type: 'checkbox', label: 'Checkbox', 119 | onChange: (value) => { 120 | console.log(value); 121 | } 122 | } 123 | ]); 124 | ``` 125 | 126 | You can also bind components representing a value to your JS variables instead of using an `onChange()` handler. For example: 127 | 128 | ```js 129 | var someNumber = 10; 130 | gui.Register([ 131 | { // A slider representing `someNumber`, constrained between 0 and 20. 132 | type: 'range', label: 'Range', 133 | min: 0, max: 20, step: 1, 134 | object: this, property: 'someNumber' 135 | }, 136 | ``` 137 | 138 | There are many points of customization here. See the docs at [/docs/api.md](/docs/api.md). A much more robust example can also be found at [example.html](/example/index.html). 139 | 140 | 141 | ### Building This Package 142 | If you want to build this package, you can run `npm install` and then `npm run build:prod`, which will create `/lib/guify.min.js`. 143 | 144 | NPM commands: 145 | 146 | - `build:prod`: Creates `/lib/guify.min.js`, the default script used by this package. 147 | - `build:dev`: Creates `/lib/guify.js`. 148 | - `develop`: Runs `build:dev` and serves the `/example` directory as a static web page. 149 | 150 | --- 151 | 152 | ## Changelog 153 | See [changelog.md](/changelog.md). 154 | 155 | 156 | ## License 157 | MIT license. See [license.md](/license.md) for specifics. 158 | 159 | 160 | ## Credit 161 | This package is largely based on [control-panel](https://github.com/freeman-lab/control-panel). 162 | For setting this up, I used [webpack-library-starter](https://github.com/krasimir/webpack-library-starter). 163 | 164 | ## Alternatives 165 | - [dat.GUI](https://github.com/dataarts/dat.gui) 166 | - [Control-Panel](https://github.com/freeman-lab/control-panel) 167 | - [Oui](https://github.com/wearekuva/oui) 168 | -------------------------------------------------------------------------------- /src/component-manager.js: -------------------------------------------------------------------------------- 1 | import { default as TitleComponent } from "./components/public/title"; 2 | import { default as RangeComponent } from "./components/public/range"; 3 | import { default as ButtonComponent } from "./components/public/button"; 4 | import { default as CheckboxComponent } from "./components/public/checkbox"; 5 | import { default as SelectComponent } from "./components/public/select"; 6 | import { default as TextComponent } from "./components/public/text"; 7 | import { default as ColorComponent } from "./components/public/color"; 8 | import { default as FolderComponent } from "./components/public/folder"; 9 | import { default as FileComponent } from "./components/public/file"; 10 | import { default as DisplayComponent } from "./components/public/display"; 11 | import { default as IntervalComponent } from "./components/public/interval"; 12 | 13 | /** 14 | * Manages the loading and instantiation of Components. 15 | */ 16 | export class ComponentManager { 17 | constructor(theme) { 18 | this.theme = theme; 19 | this.components = { 20 | "title": TitleComponent, 21 | "range": RangeComponent, 22 | "button": ButtonComponent, 23 | "checkbox": CheckboxComponent, 24 | "select": SelectComponent, 25 | "text": TextComponent, 26 | "color": ColorComponent, 27 | "folder": FolderComponent, 28 | "file": FileComponent, 29 | "display": DisplayComponent, 30 | "interval": IntervalComponent, 31 | }; 32 | 33 | } 34 | 35 | /** 36 | * Creates the component specified by `opts` and appends it to the 37 | * document as a child of `root`. 38 | * 39 | * @param {HTMLElement} [root] Parent of the created component 40 | * @param {Object} [opts] Options used to create the component 41 | */ 42 | Create(root, opts) { 43 | let initializer = this.components[opts.type]; 44 | if(initializer === undefined) { 45 | throw new Error(`No component type named '${opts.type}' exists.`); 46 | } 47 | 48 | let newComponent = new initializer(root, opts, this.theme); 49 | 50 | return newComponent; 51 | } 52 | 53 | 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/components/component-base.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "wolfy87-eventemitter"; 2 | import { v4 as uuidv4 } from "uuid"; 3 | 4 | import { default as ContainerPartial } from "./partials/container"; 5 | 6 | export default class ComponentBase extends EventEmitter { 7 | SetEnabled(enabled) { 8 | this.enabled = enabled; 9 | if (enabled) { 10 | this.container?.classList.remove("disabled"); 11 | } else { 12 | this.container?.classList.add("disabled"); 13 | } 14 | } 15 | 16 | Remove() { 17 | if (this.container) { 18 | this.container.parentNode.removeChild(this.container); 19 | } 20 | } 21 | 22 | constructor(root, opts, theme, makeContainer=true) { 23 | super(); 24 | 25 | this.root = root; 26 | this.opts = opts; 27 | this.theme = theme; 28 | 29 | this.uuid = uuidv4(); 30 | 31 | if (makeContainer) { 32 | this.container = ContainerPartial(root, opts.label, theme); 33 | } 34 | 35 | this.SetEnabled(opts.enabled || true); 36 | } 37 | } -------------------------------------------------------------------------------- /src/components/internal/menu-bar.css: -------------------------------------------------------------------------------- 1 | @import "../variables.css"; 2 | 3 | .guify-bar { 4 | background-color: var(--color-menu-bar-background); 5 | height: var(--size-menu-bar-height); 6 | width: 100%; 7 | opacity: 1.0; 8 | position: relative; 9 | top: 0; 10 | cursor: default; 11 | } 12 | 13 | .guify-bar-title { 14 | color: var(--color-menu-bar-text); 15 | text-align: center; 16 | width: 100%; 17 | position: absolute; 18 | top: 0; 19 | line-height: var(--size-menu-bar-height); 20 | -webkit-user-select: none; 21 | -moz-user-select: none; 22 | -ms-user-select: none; 23 | user-select: none; 24 | } 25 | 26 | .guify-bar-button { 27 | text-align: center; 28 | border: none; 29 | cursor: pointer; 30 | font-family: inherit; 31 | height: 100%; 32 | position: absolute; 33 | top: 0; 34 | color: var(--color-text-primary); 35 | background-color: var(--color-component-background); 36 | -webkit-user-select: none; 37 | -moz-user-select: none; 38 | -ms-user-select: none; 39 | user-select: none; 40 | margin: 0; 41 | 42 | } 43 | 44 | /* Hide default accessibility outlines since we're providing our own visual feedback */ 45 | .guify-bar-button:focus { 46 | outline: none; 47 | } 48 | .guify-bar-button::-moz-focus-inner { 49 | border: 0; 50 | } 51 | 52 | .guify-bar-button:hover, 53 | .guify-bar-button:focus { 54 | color: var(--color-text-hover); 55 | background-color: var(--color-component-foreground); 56 | } 57 | 58 | .guify-bar-button:active { 59 | color: var(--color-text-active) !important; 60 | background-color: var(--color-component-active) !important; 61 | } -------------------------------------------------------------------------------- /src/components/internal/menu-bar.js: -------------------------------------------------------------------------------- 1 | import ComponentBase from "../component-base.js"; 2 | 3 | import css from "dom-css"; 4 | import screenfull from "screenfull"; 5 | 6 | import "./menu-bar.css"; 7 | 8 | export class MenuBar extends ComponentBase { 9 | constructor(root, opts, theme) { 10 | super(root, opts, theme, false); 11 | 12 | // Create menu bar 13 | this.element = document.createElement("div"); 14 | this.element.classList.add("guify-bar"); 15 | root.appendChild(this.element); 16 | 17 | if (opts.title) { 18 | // Create a text label inside of the bar 19 | let text = this.element.appendChild(document.createElement("div")); 20 | text.classList.add("guify-bar-title"); 21 | text.innerHTML = opts.title; 22 | this.label = text; 23 | } 24 | 25 | // Make the menu collapse button 26 | let menuButton = this.element.appendChild(document.createElement("button")); 27 | menuButton.classList.add("guify-bar-button"); 28 | menuButton.innerHTML = "Controls"; 29 | css(menuButton, { 30 | left: opts.align == "left" ? "0" : "unset", 31 | right: opts.align == "left" ? "unset" : "0", 32 | }); 33 | menuButton.onclick = () => { 34 | this.emit("ontogglepanel"); 35 | }; 36 | 37 | // Make the fullscreen button 38 | if (screenfull.isEnabled) { 39 | let fullscreenButton = this.element.appendChild(document.createElement("button")); 40 | fullscreenButton.classList.add("guify-bar-button"); 41 | fullscreenButton.innerHTML = "「 」"; 42 | fullscreenButton.setAttribute("aria-label", "Toggle Fullscreen"); 43 | css(fullscreenButton, { 44 | left: opts.align == "left" ? "unset" : "0", // Place on opposite side from menuButton 45 | right: opts.align == "left" ? "0" : "unset", 46 | }); 47 | fullscreenButton.onclick = () => { 48 | this.emit("onfullscreenrequested"); 49 | }; 50 | } 51 | 52 | } 53 | 54 | SetVisible(show) { 55 | this.element.style.display = show ? "block" : "none"; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/components/internal/panel.css: -------------------------------------------------------------------------------- 1 | @import "../variables.css"; 2 | 3 | /* Container */ 4 | 5 | .guify-panel-container { 6 | position: absolute; 7 | background: var(--color-panel-background); 8 | } 9 | 10 | .guify-panel-container-scrollable { 11 | max-height: calc(100% - var(--size-menu-bar-height)); 12 | overflow: scroll; 13 | } 14 | 15 | /* Container modes (i.e. inner, outer) */ 16 | 17 | .guify-panel-container-right-inner { 18 | right: 0; 19 | left: unset; 20 | } 21 | 22 | .guify-panel-container-left-inner { 23 | right: unset; 24 | left: 0; 25 | } 26 | 27 | .guify-panel-container-right-outer { 28 | right: unset; 29 | left: 100%; 30 | } 31 | 32 | .guify-panel-container-left-outer { 33 | right: 100%; 34 | left: unset; 35 | } 36 | 37 | .guify-fullscreen .guify-panel-container-right-inner, 38 | .guify-fullscreen .guify-panel-container-right-outer { 39 | right: 0; 40 | left: unset; 41 | } 42 | 43 | .guify-fullscreen .guify-panel-container-left-inner, 44 | .guify-fullscreen .guify-panel-container-left-outer { 45 | right: unset; 46 | left: 0; 47 | } 48 | 49 | /* Panel (in container) */ 50 | 51 | .guify-panel { 52 | padding: 14px; 53 | /* Last component will have a margin, so reduce padding to account for this */ 54 | padding-bottom: calc(14px - var(--size-component-spacing)); 55 | 56 | /* all: initial; */ 57 | -webkit-user-select: none; 58 | -moz-user-select: none; 59 | -ms-user-select: none; 60 | user-select: none; 61 | cursor: default; 62 | text-align: left; 63 | box-sizing: border-box; 64 | } 65 | 66 | .guify-panel.guify-panel-hidden { 67 | height: 0px; 68 | display: none; 69 | } 70 | 71 | .guify-panel * { 72 | box-sizing: initial; 73 | -webkit-box-sizing: initial; 74 | -moz-box-sizing: initial; 75 | } 76 | 77 | .guify-panel input { 78 | display: inline; 79 | } 80 | 81 | .guify-panel a { 82 | color: inherit; 83 | text-decoration: none; 84 | } 85 | 86 | .guify-panel-toggle-button { 87 | position: absolute; 88 | top: 0; 89 | margin: 0; 90 | padding: 0; 91 | width: 15px; 92 | height: 15px; 93 | line-height: 15px; 94 | text-align: center; 95 | border: none; 96 | cursor: pointer; 97 | font-family: inherit; 98 | color: var(--color-text-primary); 99 | background-color: var(--color-component-background); 100 | 101 | -webkit-user-select: none; 102 | -moz-user-select: none; 103 | -ms-user-select: none; 104 | user-select: none; 105 | 106 | } 107 | 108 | /* Open/Close button styling */ 109 | .guify-panel-toggle-button svg { 110 | fill-opacity: 0; 111 | stroke-width: 3; 112 | stroke: var(--color-component-foreground); 113 | } 114 | 115 | /* Remove browser default outlines since we're providing our own */ 116 | .guify-panel-toggle-button:focus { 117 | outline:none; 118 | } 119 | .guify-panel-toggle-button::-moz-focus-inner { 120 | border: 0; 121 | } 122 | 123 | .guify-panel-toggle-button:hover, 124 | .guify-panel-toggle-button:focus { 125 | color: var(--color-text-hover); 126 | background-color: var(--color-component-foreground); 127 | } 128 | 129 | .guify-panel-toggle-button:active { 130 | color: var(--color-text-active); 131 | background-color:var(--color-component-active); 132 | } -------------------------------------------------------------------------------- /src/components/internal/panel.js: -------------------------------------------------------------------------------- 1 | import ComponentBase from "../component-base.js"; 2 | 3 | import css from "dom-css"; 4 | 5 | import "./panel.css"; 6 | 7 | import { default as HeaderPartial } from "../partials/header"; 8 | 9 | export class Panel extends ComponentBase { 10 | constructor(root, opts, theme) { 11 | super(root, opts, theme, false); 12 | 13 | // Container the panel will sit in 14 | this.container = root.appendChild(document.createElement("div")); 15 | this.container.classList.add("guify-panel-container"); 16 | css(this.container, { 17 | width: opts.width, 18 | opacity: opts.opacity || 1.0, 19 | }); 20 | 21 | if (opts.align == "left") { 22 | if (opts.panelMode == "outer") { 23 | this.container.classList.add("guify-panel-container-left-outer"); 24 | } else if (opts.panelMode == "inner") { 25 | this.container.classList.add("guify-panel-container-left-inner"); 26 | } 27 | } else { 28 | if (opts.panelMode == "outer") { 29 | this.container.classList.add("guify-panel-container-right-outer"); 30 | } else if (opts.panelMode == "inner") { 31 | this.container.classList.add("guify-panel-container-right-inner"); 32 | } 33 | } 34 | 35 | if (opts.panelOverflowBehavior == "scroll") { 36 | this.container.classList.add("guify-panel-container-scrollable"); 37 | } 38 | 39 | if(opts.barMode === "none") { 40 | // this._MakeToggleButton(); 41 | css(this.container, { 42 | maxHeight: "100%", 43 | }); 44 | } 45 | 46 | // Create panel inside container 47 | this.panel = this.container.appendChild(document.createElement("div")); 48 | this.panel.classList.add("guify-panel"); 49 | 50 | // Add a title to the panel 51 | if(opts.barMode === "none" && opts.title) 52 | HeaderPartial(this.panel, opts.title, theme); 53 | 54 | } 55 | 56 | /** 57 | * Makes the panel visible based on the truthiness of `show`. 58 | * @param {Bool} [show] 59 | */ 60 | SetVisible(show) { 61 | if(show){ 62 | // this.panel.style.height = Array.prototype.reduce.call(this.panel.childNodes, function(p, c) {return p + (c.offsetHeight || 0) + 5 + 1;}, 0) + 'px'; 63 | // this.panel.style.paddingTop = '14px'; 64 | // this.panel.style.paddingBottom = '8px'; 65 | this.panel.classList.remove("guify-panel-hidden"); 66 | 67 | if(this.menuButton) this.menuButton.setAttribute("alt", "Close GUI"); 68 | 69 | } 70 | else { 71 | // this.panel.style.height = '0px'; 72 | // this.panel.style.paddingTop = '0px'; 73 | // this.panel.style.paddingBottom = '0px'; 74 | this.panel.classList.add("guify-panel-hidden"); 75 | 76 | if(this.menuButton) this.menuButton.setAttribute("alt", "Open GUI"); 77 | 78 | } 79 | } 80 | 81 | /** 82 | * Toggles the visibility of the panel. 83 | */ 84 | ToggleVisible() { 85 | if (this.panel.classList.contains("guify-panel-hidden")) 86 | this.SetVisible(true); 87 | else 88 | this.SetVisible(false); 89 | } 90 | 91 | /** 92 | * Makes a show/hide button that sits at the bottom of the panel. 93 | */ 94 | _MakeToggleButton() { 95 | // Make the menu collapse button 96 | this.menuButton = this.container.appendChild(document.createElement("button")); 97 | this.menuButton.className = "guify-panel-toggle-button"; 98 | css(this.menuButton, { 99 | left: this.opts.align == "left" ? "0px" : "unset", 100 | right: this.opts.align == "left" ? "unset" : "0px", 101 | }); 102 | 103 | this.menuButton.onclick = () => { 104 | this.ToggleVisible(); 105 | }; 106 | 107 | // Defocus on mouse up (for non-accessibility users) 108 | this.menuButton.addEventListener("mouseup", () => { 109 | this.menuButton.blur(); 110 | }); 111 | 112 | this.menuButton.innerHTML = ` 113 | 114 | 115 | 116 | `; 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/components/internal/toast-area.css: -------------------------------------------------------------------------------- 1 | @import "../variables.css"; 2 | 3 | .guify-toast-notification { 4 | box-sizing: border-box; 5 | position: relative; 6 | width: 100%; 7 | /* height: 20px; */ 8 | padding: 8px; 9 | padding-left: 20px; 10 | padding-right: 20px; 11 | text-align: center; 12 | 13 | font-family: var(--font-family); 14 | font-size: var(--font-size); 15 | font-weight: var(--font-weight); 16 | } 17 | 18 | .guify-toast-area .guify-toast-notification:nth-child(odd) { 19 | color: var(--color-text-primary); 20 | background-color:var(--color-panel-background); 21 | } 22 | 23 | .guify-toast-area .guify-toast-notification:nth-child(even) { 24 | color: var(--color-text-primary); 25 | background-color: var(--color-menu-bar-background); 26 | } 27 | 28 | .guify-toast-close-button { 29 | color: var(--color-text-primary); 30 | background: transparent; 31 | position: absolute; 32 | text-align: center; 33 | margin-top: auto; 34 | margin-bottom: auto; 35 | border: none; 36 | cursor: pointer; 37 | top: 0; 38 | bottom: 0; 39 | right: 8px; 40 | } -------------------------------------------------------------------------------- /src/components/internal/toast-area.js: -------------------------------------------------------------------------------- 1 | import ComponentBase from "../component-base.js"; 2 | 3 | import css from "dom-css"; 4 | 5 | import "./toast-area.css"; 6 | 7 | /** 8 | * Represents a container div that creates and holds toast notifications. 9 | */ 10 | export class ToastArea extends ComponentBase { 11 | constructor(root, opts, theme) { 12 | super(root, opts, theme, false); 13 | 14 | // Make toast area 15 | this.element = root.appendChild(document.createElement("div")); 16 | this.element.classList.add("guify-toast-area"); 17 | css(this.element, { 18 | position: "absolute", 19 | "width": "100%", 20 | }); 21 | } 22 | 23 | /** 24 | * Makes a message that appears under the menu bar. Transitions out 25 | * over `transitionMS` milliseconds after `stayMS` milliseconds have passed. 26 | */ 27 | CreateToast(message, stayMS = 5000, transitionMS = 0) { 28 | console.log("[Toast] " + message); 29 | 30 | let toast = this.element.appendChild(document.createElement("div")); 31 | toast.classList.add("guify-toast-notification"); 32 | toast.setAttribute("aria-live", "polite"); 33 | 34 | toast.innerHTML = message; 35 | 36 | css(toast, { 37 | // Animation stuff 38 | // '-webkit-transition': 'opacity ' + transitionMS + 'ms linear', 39 | // 'transition': 'opacity ' + transitionMS + 'ms linear', 40 | }); 41 | 42 | // Make close button in toast 43 | let closeButton = toast.appendChild(document.createElement("button")); 44 | closeButton.innerHTML = "✖"; 45 | closeButton.classList.add("guify-toast-close-button"); 46 | 47 | let timeout; 48 | 49 | let TransitionOut = () => { 50 | toast.blur(); 51 | css(toast, { 52 | //'transform-style': 'flat', 53 | //'transform-style': 'preserve-3d', 54 | 55 | // Slide up 56 | // '-webkit-transition': '-webkit-transform ' + transitionMS + 'ms linear', 57 | // 'transition': 'transform ' + transitionMS + 'ms linear', 58 | // '-webkit-transform': 'translate3d(0, -100%, 0)', 59 | // 'transform:': 'translate3d(0, -100%, 0)', 60 | 61 | // Fade out 62 | //'-webkit-transition': '-webkit-opacity ' + transitionMS + 'ms linear', 63 | //'transition': 'opacity ' + transitionMS + 'ms linear', 64 | "opacity": "0", 65 | }); 66 | clearTimeout(timeout); 67 | timeout = setTimeout(() => { 68 | if(toast) 69 | toast.parentNode.removeChild(toast); 70 | }, transitionMS); 71 | }; 72 | 73 | timeout = setTimeout(TransitionOut, stayMS); 74 | 75 | closeButton.onclick = TransitionOut; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/components/partials/container.css: -------------------------------------------------------------------------------- 1 | @import "../variables.css"; 2 | 3 | .guify-component-container { 4 | position: relative; 5 | min-height: var(--size-component-height); 6 | line-height: var(--size-component-height); 7 | margin-bottom: var(--size-component-spacing); 8 | } -------------------------------------------------------------------------------- /src/components/partials/container.js: -------------------------------------------------------------------------------- 1 | import "./container.css"; 2 | 3 | // eslint-disable-next-line no-unused-vars 4 | let Container = (root, label, theme) => { 5 | let container = root.appendChild(document.createElement("div")); 6 | container.classList.add("guify-component-container"); 7 | return container; 8 | }; 9 | 10 | export default Container; 11 | -------------------------------------------------------------------------------- /src/components/partials/header.js: -------------------------------------------------------------------------------- 1 | import css from "dom-css"; 2 | 3 | export default function (root, text, theme) { 4 | var title = root.appendChild(document.createElement("div")); 5 | title.innerHTML = text; 6 | 7 | css(title, { 8 | width: "100%", 9 | textAlign: "center", 10 | color: theme.colors.textSecondary, 11 | height: "20px", 12 | marginBottom: "4px" 13 | }); 14 | 15 | return title; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/partials/label.css: -------------------------------------------------------------------------------- 1 | @import "../variables.css"; 2 | 3 | .guify-component-label { 4 | left: 0; 5 | width: calc(var(--size-label-width) - 2%); 6 | display: inline-block; 7 | margin-right: 2%; 8 | vertical-align: top; 9 | min-height: var(--size-component-height); 10 | line-height: var(--size-component-height); 11 | 12 | color: var(--color-text-primary); 13 | } 14 | 15 | /* Disabled styles */ 16 | .disabled .guify-component-label { 17 | color: var(--color-text-disabled); 18 | } -------------------------------------------------------------------------------- /src/components/partials/label.js: -------------------------------------------------------------------------------- 1 | import "./label.css"; 2 | 3 | // eslint-disable-next-line no-unused-vars 4 | export default (root, text, theme) => { 5 | var label = root.appendChild(document.createElement("div")); 6 | label.classList.add("guify-component-label"); 7 | label.innerHTML = text; 8 | return label; 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/partials/value.css: -------------------------------------------------------------------------------- 1 | @import "../variables.css"; 2 | 3 | .guify-value-input { 4 | position: absolute; 5 | background-color: var(--color-component-background); 6 | padding-left: 1%; 7 | height: var(--size-component-height); 8 | display: inline-block; 9 | overflow: hidden; 10 | border: none; 11 | 12 | color: var(--color-text-secondary); 13 | user-select: text; 14 | cursor: text; 15 | line-height: var(--size-component-height); 16 | word-break: break-all; 17 | 18 | box-sizing: border-box !important; 19 | -moz-box-sizing: border-box !important; 20 | -webkit-box-sizing: border-box !important; 21 | 22 | font-family: var(--font-family-for-input); 23 | 24 | border-radius: 0; 25 | } 26 | 27 | .guify-value-input-right { 28 | right: 0 !important; 29 | } 30 | 31 | .disabled .guify-value-input { 32 | pointer-events: none; 33 | background-color: var(--color-component-background-disabled); 34 | color: var(--color-text-disabled); 35 | } -------------------------------------------------------------------------------- /src/components/partials/value.js: -------------------------------------------------------------------------------- 1 | import css from "dom-css"; 2 | 3 | import "./value.css"; 4 | 5 | export default (root, text, theme, width, left) => { 6 | 7 | let input = root.appendChild(document.createElement("input")); 8 | input.type = "text"; 9 | input.classList.add("guify-value-input"); 10 | 11 | input.value = text; 12 | 13 | if (!left) { 14 | input.classList.add("guify-value-input-right"); 15 | } 16 | 17 | css(input, { 18 | "width": width, 19 | }); 20 | 21 | return input; 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/public/button.css: -------------------------------------------------------------------------------- 1 | @import "../variables.css"; 2 | 3 | .guify-button { 4 | box-sizing: border-box !important; 5 | color: var(--color-text-secondary); 6 | background-color: var(--color-component-background); 7 | 8 | position: absolute; 9 | text-align: center; 10 | height: var(--size-component-height); 11 | line-height: var(--size-component-height); 12 | padding-top: 0px; 13 | padding-bottom: 0px; 14 | width: calc(100% - var(--size-label-width)); 15 | border: none; 16 | cursor: pointer; 17 | right: 0; 18 | font-family: inherit; 19 | } 20 | 21 | .guify-button:focus { 22 | outline:none; 23 | } 24 | .guify-button::-moz-focus-inner { 25 | border:0; 26 | } 27 | 28 | .guify-button:hover, 29 | .guify-button:focus { 30 | color: var(--color-text-hover); 31 | background-color: var(--color-component-foreground); 32 | } 33 | 34 | .guify-button:active { 35 | color: var(--color-text-active) !important; 36 | background-color: var(--color-component-active) !important; 37 | } 38 | 39 | *.disabled > .guify-button { 40 | pointer-events: none; 41 | background-color: var(--color-component-background-disabled); 42 | color: var(--color-text-disabled); 43 | } -------------------------------------------------------------------------------- /src/components/public/button.js: -------------------------------------------------------------------------------- 1 | import ComponentBase from "../component-base.js"; 2 | 3 | import { default as LabelPartial } from "../partials/label"; 4 | 5 | import "./button.css"; 6 | 7 | export default class Button extends ComponentBase { 8 | constructor(root, opts, theme) { 9 | super(root, opts, theme); 10 | 11 | this.label = LabelPartial(this.container, "", theme); 12 | 13 | this.input = this.container.appendChild(document.createElement("button")); 14 | this.input.classList.add("guify-button"); 15 | 16 | this.input.textContent = opts.label; 17 | this.button = this.input; 18 | 19 | this.input.addEventListener("click", opts.action); 20 | 21 | // Defocus on mouse up (for non-accessibility users) 22 | this.input.addEventListener("mouseup", () => { 23 | this.input.blur(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/public/checkbox.css: -------------------------------------------------------------------------------- 1 | @import "../variables.css"; 2 | 3 | :root { 4 | --checkbox-border-width: 4px; 5 | } 6 | 7 | input[type=checkbox].guify-checkbox { 8 | opacity: 0; 9 | appearance: none; 10 | -moz-appearance: none; 11 | -webkit-appearance: none; 12 | margin: 0; 13 | border-radius: 0; 14 | border: none; 15 | cursor: pointer; 16 | } 17 | 18 | input[type=checkbox].guify-checkbox + label { 19 | margin: 0; 20 | } 21 | 22 | input[type=checkbox].guify-checkbox + label:before { 23 | content: ""; 24 | display: inline-block; 25 | width: var(--size-component-height); 26 | height: var(--size-component-height); 27 | padding: 0; 28 | margin: 0; 29 | vertical-align: middle; 30 | background-color: var(--color-component-background); 31 | border-radius: 0px; 32 | cursor: pointer; 33 | box-sizing: content-box; 34 | -moz-box-sizing: content-box; 35 | -webkit-box-sizing: content-box; 36 | 37 | } 38 | 39 | /* Hover style */ 40 | input[type=checkbox].guify-checkbox:hover:not(:disabled) + label:before { 41 | width: calc(var(--size-component-height) - (var(--checkbox-border-width) * 2)); 42 | height: calc(var(--size-component-height) - (var(--checkbox-border-width) * 2)); 43 | background-color: var(--color-component-background-hover); 44 | border: solid 4px var(--color-component-background); 45 | } 46 | 47 | /* Checked style */ 48 | input[type=checkbox]:checked.guify-checkbox + label:before { 49 | width: calc(var(--size-component-height) - (var(--checkbox-border-width) * 2)); 50 | height: calc(var(--size-component-height) - (var(--checkbox-border-width) * 2)); 51 | background-color: var(--color-component-foreground); 52 | border: solid var(--checkbox-border-width) var(--color-component-background); 53 | } 54 | 55 | /* Focused and checked */ 56 | input[type=checkbox]:checked.guify-checkbox:focus + label:before { 57 | width: calc(var(--size-component-height) - (var(--checkbox-border-width) * 2)); 58 | height: calc(var(--size-component-height) - (var(--checkbox-border-width) * 2)); 59 | background-color: var(--color-component-foreground); 60 | border: solid var(--checkbox-border-width) var(--color-component-background-hover); 61 | } 62 | 63 | /* Focus and unchecked */ 64 | input[type=checkbox].guify-checkbox:focus + label:before { 65 | background-color: var(--color-component-background-hover); 66 | } 67 | 68 | /* Disabled styles */ 69 | .disabled input[type=checkbox].guify-checkbox + label { 70 | pointer-events: none; 71 | } 72 | .disabled input[type="checkbox"].guify-checkbox + label::before { 73 | pointer-events: none; 74 | background-color: var(--color-component-background-disabled); 75 | } -------------------------------------------------------------------------------- /src/components/public/checkbox.js: -------------------------------------------------------------------------------- 1 | import ComponentBase from "../component-base.js"; 2 | 3 | import { default as LabelPartial } from "../partials/label"; 4 | 5 | import "./checkbox.css"; 6 | 7 | export default class Checkbox extends ComponentBase { 8 | constructor (root, opts, theme) { 9 | super(root, opts, theme); 10 | 11 | this.label = LabelPartial(this.container, opts.label, theme); 12 | 13 | this.input = this.container.appendChild(document.createElement("input")); 14 | this.input.id = "guify-checkbox-" + opts.label + this.uuid; 15 | this.input.type = "checkbox"; 16 | this.input.checked = opts.initial; 17 | this.input.classList.add("guify-checkbox"); 18 | // Add ARIA attribute to input based on label text 19 | if(opts.label) this.input.setAttribute("aria-label", opts.label); 20 | 21 | // This is a HTML `