├── src ├── styles │ ├── host.scss │ ├── _reset.scss │ ├── _utils.scss │ ├── styles.scss │ ├── _input_text.scss │ ├── _switch.scss │ ├── _input_radio.scss │ ├── _positions.scss │ ├── host │ │ └── _color-picker.scss │ ├── _range.scss │ ├── _control.scss │ ├── _select.scss │ ├── _input_color.scss │ ├── _toggleIcon.scss │ ├── _knob.scss │ └── _knobs.scss ├── utils │ ├── isObject.js │ ├── isModernBrowser.js │ ├── getKnobsGroups.js │ ├── parseHTML.js │ ├── mergeDeep.js │ ├── extend.js │ └── EventDispatcher.js ├── defaults.js ├── persist.js ├── cloneKnobs.js ├── templates.js ├── events.js └── knobs.js ├── .gitignore ├── demo.apng ├── demo1.apng ├── rollup.config.prod.mjs ├── rollup.config.dev.mjs ├── TODO.md ├── package.json ├── index.html ├── README.md ├── LICENSE └── knobs.min.js /src/styles/host.scss: -------------------------------------------------------------------------------- 1 | @import './host/color-picker'; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | TODO.md 4 | .vscode -------------------------------------------------------------------------------- /demo.apng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yairEO/knobs/HEAD/demo.apng -------------------------------------------------------------------------------- /demo1.apng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yairEO/knobs/HEAD/demo1.apng -------------------------------------------------------------------------------- /src/utils/isObject.js: -------------------------------------------------------------------------------- 1 | export default obj => (obj+"") === "[object Object]" 2 | -------------------------------------------------------------------------------- /src/utils/isModernBrowser.js: -------------------------------------------------------------------------------- 1 | export default () => window.CSS && CSS.supports('top', 'var(--a)') 2 | -------------------------------------------------------------------------------- /src/styles/_reset.scss: -------------------------------------------------------------------------------- 1 | label, button, input{ cursor:pointer; font:12px Arial, sans-serif; } 2 | 3 | body, form{ padding:0; margin:0; } 4 | 5 | -------------------------------------------------------------------------------- /src/styles/_utils.scss: -------------------------------------------------------------------------------- 1 | [css-util-before]::before{ 2 | content: ''; 3 | opacity: .2; 4 | position: absolute; 5 | top:0; right:0; bottom:0; left:0; 6 | } -------------------------------------------------------------------------------- /src/utils/getKnobsGroups.js: -------------------------------------------------------------------------------- 1 | import isObject from './isObject' 2 | 3 | export default knobs => knobs.reduce((acc, knobData) => { 4 | if( !isObject(knobData ) && acc[acc.length - 1].length ) acc.push([]) 5 | acc[acc.length - 1].push(knobData) 6 | return acc 7 | }, [[]]) 8 | -------------------------------------------------------------------------------- /src/utils/parseHTML.js: -------------------------------------------------------------------------------- 1 | /** 2 | * utility method 3 | * https://stackoverflow.com/a/35385518/104380 4 | * @param {String} s [HTML string] 5 | * @return {Object} [DOM node] 6 | */ 7 | export default function( s ){ 8 | var parser = new DOMParser(), 9 | node = parser.parseFromString(s.trim(), "text/html"); 10 | 11 | return node.body.firstElementChild; 12 | } -------------------------------------------------------------------------------- /src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @import './reset'; 2 | @import './utils'; 3 | @import './knobs'; 4 | @import './knob'; 5 | @import './toggleIcon'; 6 | @import './positions'; 7 | @import './select'; 8 | @import './range'; // overrides 9 | @import '@yaireo/ui-range/ui-range.scss'; 10 | @import './switch'; // overrides 11 | @import '@yaireo/ui-switch/src/switch.scss'; 12 | @import './control'; 13 | @import './input_color'; 14 | @import './input_text'; 15 | @import './input_radio'; -------------------------------------------------------------------------------- /src/defaults.js: -------------------------------------------------------------------------------- 1 | export default { 2 | visible: 0, 3 | live: true, 4 | theme: { 5 | flow: 'horizontal', 6 | styles: '', 7 | RTL: false, 8 | position: 'top right', 9 | primaryColor: '#0366D6', 10 | "range-value-background": '#FFF', 11 | "base-color": '#000', // (HS)L - the lightness is defined by default "--base-color-l: 0%" 12 | // "base-color-l": '0%', // HSL lightness" 13 | // background: "rgba(0,0,0,1)", 14 | textColor: "white", 15 | border: 'none', 16 | } 17 | } -------------------------------------------------------------------------------- /src/styles/_input_text.scss: -------------------------------------------------------------------------------- 1 | /* wrapper for "color" inputs */ 2 | label[data-type="text"], 3 | label[data-type="number"]{ 4 | input{ 5 | cursor: text; 6 | padding: 5px; 7 | border-radius: 3px; 8 | color: var(--textColor); 9 | outline: none; 10 | border: 0; 11 | background: var(--opaqueColor-15); 12 | 13 | &:focus{ 14 | box-shadow: 0 0 0 1px HSL(var(--base-color), calc(var(--base-color-l) + 22%)); 15 | } 16 | 17 | &:invalid{ 18 | box-shadow: 0 0 0 1px #d75d4a inset; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/mergeDeep.js: -------------------------------------------------------------------------------- 1 | import isObject from './isObject' 2 | 3 | const mergeDeep = (target, ...sources) => { 4 | if (!sources.length) return target; 5 | const source = sources.shift(); 6 | 7 | if (isObject(target) && isObject(source)) { 8 | for (const key in source) { 9 | if (isObject(source[key])) { 10 | if (!target[key]) Object.assign(target, { [key]: {} }); 11 | mergeDeep(target[key], source[key]); 12 | } else { 13 | Object.assign(target, { [key]: source[key] }); 14 | } 15 | } 16 | } 17 | 18 | return mergeDeep(target, ...sources); 19 | } 20 | 21 | export default mergeDeep; 22 | -------------------------------------------------------------------------------- /src/styles/_switch.scss: -------------------------------------------------------------------------------- 1 | .knobs{ 2 | &[data-flow='compact']{ 3 | .switch{ 4 | --size: 10px; 5 | --thumb-scale: 1.3; 6 | 7 | &__gfx{ 8 | padding: 0; 9 | } 10 | } 11 | } 12 | 13 | .switch{ 14 | --color-bg: #444; 15 | --color-bg-on: #444; 16 | --thumb-color-off: #d75d4a; 17 | --thumb-color-on: #4ec964; 18 | --thumb-scale: 1.1; 19 | --width-multiplier: 2.5; 20 | --thumb-animation-pad: 15%; 21 | --size: 1em; 22 | 23 | .switch__gfx{ 24 | background: none; 25 | border: 1px solid var(--bg, var(--color-bg)); 26 | } 27 | 28 | input:focus + div{ outline: none; } 29 | } 30 | } -------------------------------------------------------------------------------- /src/utils/extend.js: -------------------------------------------------------------------------------- 1 | import isObject from './isObject' 2 | 3 | export function extend( o, o1, o2) { 4 | if( !(o instanceof Object) ) o = {}; 5 | 6 | copy(o, o1); 7 | if( o2 ) 8 | copy(o, o2) 9 | 10 | function copy(a,b){ 11 | // copy o2 to o 12 | for( var key in b ) 13 | if( b.hasOwnProperty(key) ){ 14 | if( isObject(b[key]) ){ 15 | if( !isObject(a[key]) ) 16 | a[key] = Object.assign({}, b[key]); 17 | else 18 | copy(a[key], b[key]) 19 | } 20 | else 21 | a[key] = b[key]; 22 | } 23 | } 24 | 25 | return o; 26 | } -------------------------------------------------------------------------------- /src/styles/_input_radio.scss: -------------------------------------------------------------------------------- 1 | label[data-type="radio"]{ 2 | > .knobs__knob__inputWrap{ 3 | display: flex; 4 | gap: var(--radio-group-gap, 1.5em); 5 | align-items: center; 6 | justify-content: flex-end; 7 | 8 | > label{ 9 | flex: 0; 10 | display: inline-flex; 11 | gap: .5em; 12 | align-items: center; 13 | 14 | &:hover{ 15 | > *:not(input){ 16 | opacity: .7; 17 | } 18 | } 19 | 20 | input{ 21 | margin: 0; 22 | 23 | ~ * { 24 | opacity: .5; 25 | transition: .25s; 26 | } 27 | 28 | &:checked ~ * { 29 | opacity: 1; 30 | transition: 0s; 31 | } 32 | } 33 | } 34 | 35 | svg{ 36 | fill: var(--textColor); 37 | height: 20px; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/styles/_positions.scss: -------------------------------------------------------------------------------- 1 | #knobsToggle:checked + .knobs{ 2 | &[data-position~='top']{ 3 | .knobs__bg{ 4 | bottom: auto; 5 | } 6 | } 7 | 8 | &[data-position~='right']{ 9 | .knobs__bg{ 10 | left: auto; 11 | } 12 | } 13 | 14 | 15 | 16 | &[data-position~='bottom']{ 17 | > label{ 18 | top: auto; 19 | bottom: var(--offset); 20 | } 21 | 22 | .knobs__bg{ 23 | top: auto; 24 | } 25 | } 26 | 27 | 28 | 29 | &[data-position~='left']{ 30 | > label{ 31 | right: auto; 32 | left: var(--offset); 33 | } 34 | 35 | .knobs__bg{ 36 | right: auto; 37 | } 38 | 39 | &[data-position~='bottom']{ 40 | --control-left-pad: var(--toggleSize); 41 | } 42 | } 43 | } 44 | 45 | .knobs[data-position~='left']{ 46 | --LTR-Bool: -1; 47 | } -------------------------------------------------------------------------------- /src/styles/host/_color-picker.scss: -------------------------------------------------------------------------------- 1 | .color-picker{ 2 | h1{ 3 | margin: 0 0 .5em; 4 | font-size: 1em; 5 | text-align: center; 6 | } 7 | &.hidden{ 8 | opacity: 0; 9 | pointer-events: none; 10 | } 11 | } 12 | 13 | .color-picker[positioned]{ 14 | --x: calc(var(--pos-left) + var(--window-scroll-x)); 15 | --y: calc(var(--pos-top) + var(--window-scroll-y)); 16 | position: absolute; 17 | z-index: 9999991; 18 | border-radius: 10px; 19 | padding: .5em; 20 | box-shadow: 0 5px 20px #00000044; 21 | backdrop-filter: blur(3px); 22 | background: white; 23 | top: 0; 24 | left: 0; 25 | transform: translate(calc(var(--x) * 1px), 26 | calc(var(--y) * 1px)); 27 | 28 | @media only screen and (max-device-width : 640px) { 29 | max-width: 70%; 30 | } 31 | 32 | &:not(.hidden) ~ .knobsIframe{ 33 | pointer-events: none; 34 | filter: blur(2px); 35 | } 36 | } -------------------------------------------------------------------------------- /src/styles/_range.scss: -------------------------------------------------------------------------------- 1 | // https://github.com/yairEO/ui-range 2 | // overrides: 3 | .knobs .range-slider { 4 | --fill-color: var(--range-track-color); 5 | --primaryColor: var(--range-value-background); 6 | 7 | --value-active-color: var(--range-track-color); 8 | --value-background: transparent; 9 | --value-background-hover: white; 10 | --value-offset-y: 9px; 11 | 12 | --progress-background: #444; 13 | --thumb-size: 14px; 14 | --track-height: calc(var(--thumb-size)/3); 15 | --ticks-thickness: 1px; 16 | --ticks-height: 0px; 17 | --show-min-max: none; 18 | --thumb-color: var(--range-track-color); 19 | --thumb-shadow: 0 0 3px rgba(0,0,0,.2), 0 0 0 calc(var(--thumb-size)/6) inset white; 20 | --thumb-shadow-active: 0 0 3px rgba(0,0,0,.2), 0 0 0 calc(var(--thumb-size)/4) inset white; 21 | 22 | color: transparent; 23 | 24 | > input:hover + output { 25 | box-shadow: 0 0 0 3px var(--value-background), 26 | 0 0 6px 4px var(--background); 27 | } 28 | } -------------------------------------------------------------------------------- /rollup.config.prod.mjs: -------------------------------------------------------------------------------- 1 | import scss from 'rollup-plugin-scss' 2 | import babel from '@rollup/plugin-babel' 3 | import terser from '@rollup/plugin-terser'; 4 | import cleanup from 'rollup-plugin-cleanup' 5 | import { nodeResolve } from '@rollup/plugin-node-resolve' 6 | import commonjs from '@rollup/plugin-commonjs' 7 | import {readFileSync} from 'fs' 8 | 9 | const pkg = JSON.parse(readFileSync('./package.json')) 10 | const banner = `/*! Knobs ${pkg.version} MIT | https://github.com/yairEO/knobs */\n`; 11 | 12 | export default [ 13 | { 14 | input: 'src/knobs.js', 15 | output: { 16 | banner, 17 | file: 'knobs.min.js', 18 | format: 'umd', 19 | name: 'Knobs', 20 | }, 21 | plugins: [ 22 | terser(), 23 | babel({ babelHelpers: 'bundled' }), 24 | scss({ output: false, outputStyle: 'compressed', watch: 'src/styles', }), 25 | cleanup(), 26 | nodeResolve(), 27 | commonjs() 28 | ] 29 | } 30 | ] 31 | 32 | -------------------------------------------------------------------------------- /src/styles/_control.scss: -------------------------------------------------------------------------------- 1 | /* bottom controls (apply/reset) */ 2 | .knobs__controls { 3 | display: flex; 4 | align-items: center; 5 | opacity: 0; 6 | flex-direction: row-reverse; 7 | // border-top: 1px solid var(--textColor); 8 | margin: var(--side-pad) 9 | var(--control-right-pad, var(--side-pad)) 10 | 5px 11 | var(--control-left-pad, var(--side-pad)); 12 | position: relative; 13 | z-index: 1; 14 | 15 | > input { 16 | color: var(--textColor); 17 | border: 0; 18 | background: none; 19 | margin-left: 1em; 20 | line-height: 1; 21 | padding: 5px 8px; 22 | border-radius: 3px; 23 | position: relative; 24 | 25 | &:hover:not(:active){ 26 | background: var(--opaqueColor-15); 27 | } 28 | } 29 | } 30 | 31 | .poweredBy{ 32 | margin-right: auto; 33 | text-decoration: none; 34 | color: inherit; 35 | padding: 3px; 36 | font-size: 10px; 37 | opacity: .5; 38 | transition: .15s; 39 | } 40 | 41 | .poweredBy:hover{ 42 | color: var(--primaryColor); 43 | opacity: 1; 44 | } 45 | -------------------------------------------------------------------------------- /rollup.config.dev.mjs: -------------------------------------------------------------------------------- 1 | import scss from 'rollup-plugin-scss' 2 | import babel from '@rollup/plugin-babel' 3 | import cleanup from 'rollup-plugin-cleanup' 4 | import serve from 'rollup-plugin-serve' 5 | import livereload from 'rollup-plugin-livereload' 6 | import { nodeResolve } from '@rollup/plugin-node-resolve' 7 | import commonjs from '@rollup/plugin-commonjs' 8 | import {readFileSync} from 'fs' 9 | 10 | const pkg = JSON.parse(readFileSync('./package.json')) 11 | const banner = `/*! Knobs ${pkg.version} MIT | https://github.com/yairEO/knobs */\n`; 12 | 13 | export default [ 14 | { 15 | input: 'src/knobs.js', 16 | output: { 17 | banner, 18 | file: 'knobs.min.js', 19 | format: 'umd', 20 | name: 'Knobs', 21 | }, 22 | plugins: [ 23 | serve(), // index.html should be in root of project 24 | livereload({ watch:'src', delay:1500, exts: [ 'html', 'js', 'scss', 'css' ] }), 25 | babel({ babelHelpers: 'bundled' }), 26 | scss({ output: false, watch: 'src/styles', }), 27 | cleanup(), 28 | nodeResolve(), 29 | commonjs() 30 | ] 31 | } 32 | ] 33 | 34 | -------------------------------------------------------------------------------- /src/styles/_select.scss: -------------------------------------------------------------------------------- 1 | .knobs label[data-type='select']{ 2 | .knobs__knob__inputWrap{ 3 | &::before{ 4 | // "--value" variable only exists for valid select values. if this variable exists, hide this psuedo element. 5 | // this calc must reside within a varible or the space between the minus signs is removed 6 | --hide: Calc(var(--value) - var(--value)); 7 | 8 | content: 'N/A'; 9 | font-style: italic; 10 | opacity: var(--hide); 11 | filter: opacity(0.5); 12 | position: absolute; 13 | right: 2em; 14 | pointer-events: none; 15 | } 16 | } 17 | 18 | &::after{ 19 | content: '❯'; 20 | pointer-events: none; 21 | align-self: center; 22 | transform: translate(-100%, var(--offset-y, -1px)) rotate(90deg) scaleY(.8); 23 | transition: .1s; 24 | } 25 | 26 | &:hover{ 27 | --offset-y: 1px; 28 | } 29 | 30 | select{ 31 | font: inherit; 32 | background: none; 33 | color: var(--textColor); 34 | padding: 3px 0; 35 | cursor: pointer; 36 | border: none; 37 | outline: none; 38 | text-align-last: right; 39 | appearance: none; 40 | padding: 0 1.1em 0 0; 41 | 42 | } 43 | 44 | option{ 45 | background: var(--background); 46 | } 47 | } -------------------------------------------------------------------------------- /src/styles/_input_color.scss: -------------------------------------------------------------------------------- 1 | /* wrapper for "color" inputs */ 2 | label[data-type="color"]{ 3 | > .knobs__knob__inputWrap{ 4 | & > div{ 5 | display: inline-block; 6 | border-radius: 5px; 7 | overflow: hidden; 8 | width: calc(var(--color-size) * 4); 9 | height: calc(var(--color-size) - 2px); // same as border 10 | // transition: .2s var(--in-easing); 11 | transform-origin: center right; 12 | background: var(--background) repeating-conic-gradient(#FFFFFF33 0% 25%, transparent 0% 50%) 0/6px 6px; // checkboard pattern 13 | } 14 | } 15 | 16 | &:hover > .knobs__knob__inputWrap > div{ 17 | animation: colorHover .5s ease-out; 18 | // box-shadow: 0 0 0 1px #FFFFFF66; // needed in case a color is same as knobs background 19 | } 20 | 21 | input{ 22 | width: 100%; 23 | height: 100%; 24 | border: 0; 25 | background: var(--value); 26 | color: transparent; 27 | outline: none; 28 | caret-color: transparent; 29 | text-transform: uppercase; 30 | font-weight: 600; 31 | &::selection{ 32 | color: transparent; 33 | } 34 | } 35 | } 36 | 37 | @keyframes colorHover{ 38 | 20%{ transform: scale(1.2) } 39 | 40%{ transform: scale(1) } 40 | 60%{ transform: scale(1.1) } 41 | } -------------------------------------------------------------------------------- /src/persist.js: -------------------------------------------------------------------------------- 1 | const VERSION = 1; // current version of persisted data. if code change breaks persisted data, verison number should be bumped. 2 | const STORE_KEY = '@yaireo/knobs/knobs' 3 | 4 | const isStringOrNumber = a => typeof a == 'string' || typeof a == 'number'; 5 | 6 | export function getPersistedData(){ 7 | // if "persist" is "false", do not save to localstorage 8 | let _store = this.settings.persist, 9 | customKey = isStringOrNumber(_store) ? '/'+_store : '', 10 | persistedData, 11 | versionMatch = localStorage.getItem(`${STORE_KEY + customKey}/v`) == VERSION 12 | 13 | if( versionMatch ){ 14 | try{ persistedData = JSON.parse(localStorage[STORE_KEY + customKey]) } 15 | catch(err){} 16 | } 17 | 18 | return persistedData 19 | } 20 | 21 | export function setPersistedData(){ 22 | // if "persist" is "false", do not save to localstorage 23 | let _store = this.settings.persist, 24 | customKey = isStringOrNumber(_store) ? '/'+_store : ''; 25 | 26 | // when wishing to set data: 27 | if ( _store ){ 28 | let persistedData = JSON.stringify(this.knobs) 29 | 30 | localStorage.setItem(`${STORE_KEY + customKey}/v`, VERSION) // pesisted 31 | localStorage.setItem(STORE_KEY + customKey, persistedData) 32 | 33 | dispatchEvent( new Event('storage') ) 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/utils/EventDispatcher.js: -------------------------------------------------------------------------------- 1 | import mergeDeep from './mergeDeep' 2 | 3 | export default function EventDispatcher(){ 4 | // Create a DOM EventTarget object 5 | var target = document.createTextNode('') 6 | 7 | function addRemove(op, events, cb){ 8 | if( cb ) 9 | events.split(/\s+/g).forEach(name => target[op + 'EventListener'].call(target, name, cb)) 10 | } 11 | 12 | // Pass EventTarget interface calls to DOM EventTarget object 13 | return { 14 | off(events, cb){ 15 | addRemove('remove', events, cb) 16 | return this 17 | }, 18 | 19 | on(events, cb){ 20 | if(cb && typeof cb == 'function') 21 | addRemove('add', events, cb) 22 | return this 23 | }, 24 | 25 | trigger(eventName, data, opts){ 26 | var e; 27 | 28 | opts = opts || { 29 | cloneData:true 30 | } 31 | 32 | if( !eventName ) return; 33 | 34 | else{ 35 | try { 36 | var eventData = typeof data === 'object' 37 | ? data 38 | : {value:data}; 39 | 40 | eventData = opts.cloneData ? mergeDeep({}, eventData) : eventData 41 | eventData.knobs = this 42 | 43 | // TODO: move the below to the "mergeDeep" function 44 | if( data instanceof Object ) 45 | for( var prop in data ) 46 | if(data[prop] instanceof HTMLElement) 47 | eventData[prop] = data[prop] 48 | 49 | e = new CustomEvent(eventName, {"detail":eventData}) 50 | } 51 | catch(err){ console.warn(err) } 52 | 53 | target.dispatchEvent(e); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/cloneKnobs.js: -------------------------------------------------------------------------------- 1 | import isObject from './utils/isObject' 2 | 3 | const generateId = () => Math.random().toString(36).slice(-6); 4 | 5 | export default function(knobs, persistedData){ 6 | return knobs.map(k => { 7 | if( k && k.type ){ 8 | k.__name = k.__name || (k.label?.replaceAll(/[^a-zA-Z0-9 ]/g, '').replaceAll(' ','-').toLowerCase() || '') + '-' + generateId() 9 | k.defaultValue = k.defaultValue ?? k.value ?? this.getKnobValueFromCSSVar(k) ?? ''// value to revert to, if wished to reset 10 | k.defaultChecked = k.defaultChecked ?? !!k.checked 11 | 12 | k.isToggled = k.isToggled ?? true; 13 | // if( !this.settings.knobsToggle && k.isToggled === false ) 14 | // delete k.isToggled 15 | 16 | if( persistedData ){ 17 | // if current iterated knob exists in the persisted data array, use it 18 | let thisKnobPersistedData = persistedData.find(a => a.label && a.label == k.label) 19 | 20 | if( thisKnobPersistedData ){ 21 | if( k.defaultValue ) 22 | thisKnobPersistedData.defaultValue = k.defaultValue 23 | // override persisted "select" knob options to make sure they are the latest 24 | if( k.options ){ 25 | thisKnobPersistedData.options = k.options 26 | } 27 | 28 | return thisKnobPersistedData 29 | } 30 | } 31 | 32 | // cast to type "number" if needed (per input type) 33 | if( k.type == 'range' ){ 34 | k.value = +k.value || k.defaultValue 35 | k.defaultValue = +k.defaultValue 36 | } 37 | else if( k.type == 'checkbox' ){ 38 | k.checked = k.checked || k.defaultChecked 39 | } 40 | else{ 41 | // value is not necessarily defined, if is wished to be feched from the CSS automatically 42 | k.value = k.value || k.defaultValue 43 | } 44 | } 45 | 46 | if( k.render ) { 47 | k.__name = "custom-" + generateId() 48 | } 49 | 50 | return k.cssVar 51 | ? {...k, cssVar:[...k.cssVar]} 52 | : isObject(k) // labels are not objects, so use us-is 53 | ? {...k} 54 | : k 55 | }) 56 | } -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - [x] Allow setting units in a way that it's a part of knob's title but not part of the variable (presentational-only) 2 | - [x] Auto-detect initial CSS variables values from the target element, and populate knobs, if "value" prop is not specified 3 | - [x] Support wheel event for sliders 4 | - [x] Change native color picker to custom one 5 | - [x] Persist changes (setting) to localstorage 6 | - [x] Add a toggle button next to each knob (if supplied in setting to allow it) 7 | - [x] Make knobs setting to allow creating a non-iframe knobs element (which can be used/places however desired) 8 | - [x] automatically-detected values are not working as expecetd (maybe detecting the wrong element) 9 | - [x] max height - allow scroll of knobs when height is more than viewport (use resize observer) 10 | - [x] fix color knobs unable to revert 11 | - [x] Fix checkbox alignment when in compact-mode 12 | - [x] Add ability to pre-define a knob as non-checked 13 | - [x] Presist knob checkbox (enable/disable) as well 14 | - [x] Add title attribute (the input's value) to color input elements 15 | - [x] add dropdown knob (select) 16 | - [x] Allows separator line without any text 17 | - [x] when changing slider value & disabling & enabling - does not re-apply the slider's value 18 | - [x] color inputs - add knob settings for `default format` & 19 | - [x] color inputs - expose color-converion functions from color-picker so they could be used in a knob's `onChange` callback 20 | - [x] Allow custom radio inputs (as icons for example) 21 | - [x] Add Label titles - when hovering a label, should show a tooltip with optional explanation 22 | - [ ] color inputs - add `formats` knob property to enforce only certain formats (requires "color-picker" feature support) 23 | - [ ] Allow customizing the in/out transition duration 24 | - [ ] Add custom buttons 25 | - [ ] Add pre-defined value point buttons for the slider, like here: https://codepen.io/thebabydino/pen/zYvEqMd 26 | - [ ] outline which DOM node (element) is affected by the knob (if custom target is specified) 27 | - [ ] Optionally Add (-) (+) buttons to range sliders for sensetive changes (vaie Knob JSON) 28 | - [ ] Allow definding "options" alongside "slider" to show the dropdown at the same line as the slider 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaireo/knobs", 3 | "version": "1.3.6", 4 | "homepage": "https://github.com/yairEO/knobs", 5 | "description": "UI knobs controllers for JS/CSS live manipulation of various parameters", 6 | "keywords": [ 7 | "knobs", 8 | "controller", 9 | "ui", 10 | "component", 11 | "visual", 12 | "parameters" 13 | ], 14 | "scripts": { 15 | "start": "rollup -c rollup.config.dev.mjs -w", 16 | "build:prod": "npm run clean && npm run bundle:prod", 17 | "clean": "rm -rf knobs.min.js", 18 | "bundle:prod": "rollup -c rollup.config.prod.mjs", 19 | "test": "echo \"No test specified\"", 20 | "header": "headr knobs.min.js -o=knobs.min.js --version --homepage", 21 | "version": "npm run build:prod && npm run header && git add .", 22 | "prepublishOnly": "pkg-ok" 23 | }, 24 | "main": "./knobs.min.js", 25 | "files": [ 26 | "knobs.min.js", 27 | "src/styles" 28 | ], 29 | "license": "MIT", 30 | "browserslist": [ 31 | ">3% and ie 11", 32 | "not dead", 33 | "not ie < 11", 34 | "not IE_Mob 11", 35 | "not op_mini all" 36 | ], 37 | "author": { 38 | "name": "Yair Even-Or", 39 | "email": "vsync.design@gmail.com" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com/yairEO/knobs.git" 44 | }, 45 | "bugs": { 46 | "url": "https://github.com/yaireo/knobs/issues" 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.21.4", 50 | "@babel/preset-env": "^7.21.4", 51 | "@rollup/plugin-babel": "^6.0.3", 52 | "@rollup/plugin-commonjs": "^24.0.1", 53 | "@rollup/plugin-node-resolve": "^15.0.1", 54 | "@rollup/plugin-terser": "^0.4.0", 55 | "headr": "^0.0.4", 56 | "rollup": "^3.20.2", 57 | "rollup-plugin-cleanup": "^3.2.1", 58 | "rollup-plugin-livereload": "^2.0.5", 59 | "rollup-plugin-scss": "^4.0.0", 60 | "rollup-plugin-serve": "^2.0.2", 61 | "sass": "^1.60.0" 62 | }, 63 | "dependencies": { 64 | "@yaireo/color-picker": "~0.12.0", 65 | "@yaireo/position": "^1.1.1", 66 | "@yaireo/ui-range": "^2.1.15", 67 | "@yaireo/ui-switch": "^1.0.5" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/styles/_toggleIcon.scss: -------------------------------------------------------------------------------- 1 | .leversIcon{ 2 | $size: 5px; 3 | 4 | width: 56px; 5 | transform: scale(.4); 6 | transform-origin: 0 0; 7 | 8 | > div{ 9 | display: flex; 10 | align-items: center; 11 | transition: transform .2s ease; 12 | 13 | &:nth-child(1){ 14 | &::before{ flex: .33; transition-delay: .3s; } 15 | } 16 | 17 | &:nth-child(2){ 18 | margin: 2px 0; 19 | &::after{ flex: .33; } 20 | } 21 | 22 | &:nth-child(3){ 23 | &::before{ flex: .8; transition-delay: .1s; } 24 | } 25 | 26 | > b{ 27 | display: inline-block; 28 | width: $size*1.5; 29 | height: $size*1.5; 30 | border-radius: 50%; 31 | border: #{$size - 1px} solid currentColor; 32 | margin: 0 $size; 33 | } 34 | 35 | &::before, 36 | &::after{ 37 | content: ''; 38 | height: $size; 39 | background: currentColor; 40 | border-radius: $size; 41 | flex: 1; 42 | transition: flex .1s ease; 43 | } 44 | 45 | &::after{ 46 | flex: auto; 47 | opacity: .33; 48 | } 49 | } 50 | } 51 | 52 | @keyframes leversIcon{ 53 | 30%{ flex: .2; } 54 | 80%{ flex: 5; } 55 | } 56 | 57 | #knobsToggle:not(:checked) + .knobs > label{ 58 | &:hover{ 59 | .leversIcon > div{ 60 | &:nth-child(1){ 61 | &::before{ animation: 1s leversIcon ease infinite; } 62 | } 63 | 64 | &:nth-child(2){ 65 | margin: 1px 0; 66 | &::after{ animation: 1s .1s leversIcon ease reverse infinite; } 67 | } 68 | 69 | &:nth-child(3){ 70 | &::before{ animation: 1.2s .15s leversIcon ease alternate infinite; } 71 | } 72 | } 73 | } 74 | } 75 | 76 | 77 | #knobsToggle:checked + .knobs > label{ 78 | --size: 18px; 79 | --offset: calc(var(--toggleOffset) + var(--size)/3); 80 | 81 | .leversIcon{ 82 | width: 65px; 83 | color: var(--textColor); 84 | transition: color .2s; 85 | transform: scale(.3) translate(0, 6px); 86 | opacity: .7; 87 | 88 | &:hover{ 89 | opacity: 1; 90 | } 91 | 92 | b{ 93 | transform: scale(0); 94 | margin: 0; 95 | width: 0; 96 | } 97 | 98 | > div{ 99 | &::after{ 100 | flex: 0; 101 | } 102 | &::before{ 103 | flex: 3; 104 | height: 8px; 105 | } 106 | 107 | &:nth-child(1){ 108 | transform: rotate(45deg); 109 | transform-origin: 20% 50%; 110 | } 111 | 112 | &:nth-child(2){ 113 | opacity: 0; 114 | } 115 | 116 | &:nth-child(3){ 117 | transform: rotate(-45deg); 118 | transform-origin: 0 0; 119 | } 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /src/styles/_knob.scss: -------------------------------------------------------------------------------- 1 | .knobs{ 2 | &[data-flow='compact']{ 3 | .knobs__knob__toggle{ 4 | align-self: flex-start; 5 | margin-top: 6px; 6 | } 7 | } 8 | 9 | &__knob{ 10 | $self: &; 11 | display: flex; 12 | justify-content: flex-end; 13 | position: relative; 14 | line-height: var(--line-height); 15 | min-height: 24px; 16 | 17 | &:hover{ 18 | #{$self}__reset{ 19 | // transform: none; 20 | } 21 | 22 | #{$self}__label__text{ 23 | opacity: 1; 24 | } 25 | } 26 | 27 | // only show the "rest" button if knob has changed. 28 | // this allows the user to know which one was changed, where many knobs exist 29 | &[data-changed]{ 30 | #{$self}__reset{ 31 | opacity: .75; 32 | pointer-events: auto; 33 | 34 | &:hover{ 35 | opacity: 1; 36 | background: var(--textColor); 37 | color: var(--background); 38 | transition: 0s; 39 | } 40 | } 41 | } 42 | 43 | &__toggle{ 44 | display: var(--knobs-toggle, none); 45 | order: 1; 46 | align-self: center; 47 | margin: 0 5px 0 0; 48 | appearance: none; 49 | width: 12px; 50 | height: 12px; 51 | outline: none; 52 | border-radius: 50%; 53 | position: relative; 54 | text-align: center; 55 | line-height: 10px; 56 | 57 | &::before{ 58 | border: 1px solid var(--textColor); 59 | opacity: .4; 60 | border-radius: 3px; 61 | } 62 | 63 | &::after{ 64 | content: ''; 65 | height: 100%; 66 | z-index: 5; 67 | width: 999px; 68 | position: absolute; 69 | left: 0; 70 | pointer-events: none; 71 | } 72 | 73 | &:hover{ 74 | &::before{ 75 | opacity: 1; 76 | } 77 | } 78 | 79 | &:checked{ 80 | &:hover ~ *{ 81 | text-decoration: line-through; 82 | transition: .15s; 83 | } 84 | 85 | &::after{ 86 | content: '✔'; 87 | color: var(--textColor); 88 | font-size: 12px; 89 | text-shadow: -1px -2px var(--background), 3px -2px var(--background); 90 | position: relative; 91 | z-index: 1; 92 | } 93 | } 94 | 95 | &:not(:checked){ 96 | ~ *{ 97 | pointer-events: none !important; 98 | filter: grayscale(50%); 99 | opacity: .4; 100 | transition: .2s; 101 | 102 | ::-webkit-slider-thumb { pointer-events: none !important; } 103 | ::-moz-slider-thumb { pointer-events: none !important; } 104 | } 105 | } 106 | } 107 | 108 | &__reset{ 109 | order: 0; 110 | pointer-events: none; 111 | margin-right: .5em; 112 | padding: 0; 113 | align-self: center; 114 | color: inherit; 115 | background: none; 116 | border: 0; 117 | cursor: pointer; 118 | opacity: .33; 119 | outline: none; 120 | border-radius: 50%; 121 | width: 2ch; 122 | height: 2ch; 123 | user-select: none; 124 | transition: .15s ease-out; 125 | } 126 | 127 | &__label__text { 128 | margin-right: 2ch; 129 | white-space: nowrap; 130 | display: flex; 131 | align-items: center; 132 | opacity: .8; 133 | transition: 80ms; 134 | 135 | // variable units (if exists) 136 | &::after{ 137 | content: attr(data-units); 138 | opacity: .5; 139 | margin-left: 1ch; 140 | } 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /src/templates.js: -------------------------------------------------------------------------------- 1 | import isObject from './utils/isObject' 2 | 3 | var settingsIcon = `