diff --git a/src/client/components/entity.jsx b/src/client/components/entity.jsx index 7f2fa59..a09d1d8 100644 --- a/src/client/components/entity.jsx +++ b/src/client/components/entity.jsx @@ -1,4 +1,5 @@ import {compose} from '@avocado/core'; +import {Entity} from '@avocado/entity'; import contempo from 'contempo'; import React from 'react'; @@ -8,19 +9,21 @@ const decorate = compose( contempo(require('./entity.raw.scss')), ); -const kitty = require('~/../fixtures/kitty.entity.json'); +const json = require('~/../fixtures/kitty.entity.json'); -const Entity = () => { - const entity = kitty; - const {traits} = entity; +const EntityComponent = (props) => { + const {context} = props; + const entity = new Entity(json); + context.add('entity', entity); + const {traits} = json; return (
- +
); }; -export default decorate(Entity); +export default decorate(EntityComponent); diff --git a/src/client/components/entity.raw.scss b/src/client/components/entity.raw.scss index 550eac9..3459220 100644 --- a/src/client/components/entity.raw.scss +++ b/src/client/components/entity.raw.scss @@ -8,4 +8,4 @@ .settings-pane { float: left; width: 36em; -} \ No newline at end of file +} diff --git a/src/client/components/persea.jsx b/src/client/components/persea.jsx index f260613..602b659 100644 --- a/src/client/components/persea.jsx +++ b/src/client/components/persea.jsx @@ -1,10 +1,13 @@ +import {Context} from '@avocado/behavior'; import {hot} from 'react-hot-loader'; import React from 'react'; import Entity from './entity'; +const context = new Context(); + const Persea = () => ( - + ); export default hot(module)(Persea); diff --git a/src/client/components/traits.jsx b/src/client/components/traits.jsx index 24d9b63..b76ddde 100644 --- a/src/client/components/traits.jsx +++ b/src/client/components/traits.jsx @@ -53,7 +53,7 @@ const ensureTypeRenderers = () => { } }; -const makeTabSelector = (type) => createSelector( +const makeTabSelector = (context, type) => createSelector( (_) => _, (trait) => { ensureTypeRenderers(); @@ -79,6 +79,7 @@ const makeTabSelector = (type) => createSelector(
createSelector( ); const Traits = (props) => { - const {traits} = props; + const {context, traits} = props; const [tabSelectorMap, setTabSelectorMap] = useState({}); useEffect(() => { const entries = Object.entries(traits); @@ -114,7 +115,7 @@ const Traits = (props) => { draft[type] = new WeakMap(); } if (!draft[type].has(traits)) { - draft[type].set(traits, makeTabSelector(type)); + draft[type].set(traits, makeTabSelector(context, type)); } } })); diff --git a/src/client/components/types/actions.raw.scss b/src/client/components/types/actions.raw.scss index 904f60b..920f95b 100644 --- a/src/client/components/types/actions.raw.scss +++ b/src/client/components/types/actions.raw.scss @@ -1,28 +1,19 @@ -.args { - margin-left: 2em; -} - -.arg { - margin-top: 0.125em; - > :first-child::after { - color: #999999; - content: ', '; - } -} - -.bracket, .paren { - color: #999999; -} - -.object { - background-color: #333333; - border: 1px solid black; - padding: 0.25em; +:scope { + margin-top: 0.5em; } li { - margin-bottom: 0.5em; + border: 1px solid rgba(255, 255, 255, 0.1); + border-bottom: none; + padding: 1em; &:last-child { - margin-bottom: 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); } -} \ No newline at end of file + &:nth-of-type(2n) { + background-color: rgba(0, 0, 0, 0.025); + } + &:nth-of-type(2n+1) { + background-color: rgba(255, 255, 255, 0.025); + } +} + diff --git a/src/client/components/types/actions.type-renderer.jsx b/src/client/components/types/actions.type-renderer.jsx index bec0a5d..d559938 100644 --- a/src/client/components/types/actions.type-renderer.jsx +++ b/src/client/components/types/actions.type-renderer.jsx @@ -12,6 +12,7 @@ const decorate = compose( ); const Actions = ({ + context, name, label, options, @@ -22,7 +23,15 @@ const Actions = ({ { value.traversals.length > 0 && ( value.traversals.map( - (traversal) =>
  • , + (traversal) => ( +
  • + +
  • + ), ) ) } diff --git a/src/client/components/types/bool.type-renderer.jsx b/src/client/components/types/bool.type-renderer.jsx index 0b137f2..0ccc665 100644 --- a/src/client/components/types/bool.type-renderer.jsx +++ b/src/client/components/types/bool.type-renderer.jsx @@ -8,7 +8,10 @@ const Bool = ({ label, value, }) => ( - + ); Bool.propTypes = { diff --git a/src/client/components/types/condition.raw.scss b/src/client/components/types/condition.raw.scss index 32f1d24..89d442b 100644 --- a/src/client/components/types/condition.raw.scss +++ b/src/client/components/types/condition.raw.scss @@ -1,11 +1,16 @@ .condition { - display: flex; - justify-content: flex-end; align-items: center; + display: flex; + flex-wrap: wrap; + justify-content: flex-end; } .operator { - margin: 0 0.5em; + margin: 0.25em 0; text-align: center; text-align-last: center; } + +.operand .steps { + display: flex; +} diff --git a/src/client/components/types/condition.type-renderer.jsx b/src/client/components/types/condition.type-renderer.jsx index f591fea..f766dc7 100644 --- a/src/client/components/types/condition.type-renderer.jsx +++ b/src/client/components/types/condition.type-renderer.jsx @@ -4,29 +4,11 @@ import PropTypes from 'prop-types'; import React from 'react'; import propertyPropTypes from './property-prop-types'; +import Value from './value.type-renderer'; -const renderValue = (value) => { - switch (value.type) { - case 'traversal': { - const {steps} = value; - return ( - - {steps.length > 0 && ( - steps.slice(1).reduce((r, step) => `${r}.${step.key}`, steps[0].key) - )} - - ); - } - case 'literal': - return ; - default: - return null; - } -}; - -const renderOperand = (operand) => ( +const renderOperand = (context, operand, type) => ( - {renderValue(operand)} + ); @@ -44,6 +26,7 @@ const decorate = compose( ); const Condition = ({ + context, name, label, options, @@ -59,6 +42,7 @@ const Condition = ({ case '>=': case '<': case '<=': + const type = -1 !== ['is', 'isnt'].indexOf(value.operator) ? 'any' : 'number'; return value.operands.slice(1).reduce( (r, operand) => ( <> @@ -66,10 +50,10 @@ const Condition = ({ - {renderOperand(operand)} + {renderOperand(context, operand, type)} ), - renderOperand(value.operands[0]), + renderOperand(context, value.operands[0], type), ); default: return undefined; diff --git a/src/client/components/types/routines.raw.scss b/src/client/components/types/routines.raw.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/client/components/types/routines.type-renderer.jsx b/src/client/components/types/routines.type-renderer.jsx new file mode 100644 index 0000000..f5859d0 --- /dev/null +++ b/src/client/components/types/routines.type-renderer.jsx @@ -0,0 +1,49 @@ +import {compose} from '@avocado/core'; +import classnames from 'classnames'; +import contempo from 'contempo'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import propertyPropTypes from './property-prop-types'; +import Actions from './actions.type-renderer'; + +const decorate = compose( + contempo(require('./routines.raw.scss')), +); + +const Routines = ({ + context, + name, + label, + options, + value, +}) => { + const entries = Object.entries(value.routines); + return ( +
    +
      + { + entries.length > 0 && ( + entries.map( + ([name, routine]) => ( +
    1. + +
    2. + ), + ) + ) + } +
    +
    + ); +}; + +Routines.propTypes = { + ...propertyPropTypes, + value: PropTypes.shape({}).isRequired, +}; + +export default { + type: 'routines', + component: decorate(Routines), +}; diff --git a/src/client/components/types/steps-lists.js b/src/client/components/types/steps-lists.js new file mode 100644 index 0000000..68cd48d --- /dev/null +++ b/src/client/components/types/steps-lists.js @@ -0,0 +1,35 @@ +import {flatten} from '@avocado/core'; + +import {typeFits, typeFromV} from './typing'; + +export const variableStepsList = (key, variable, description, type) => { + const steps = []; + if (typeFits(type, description.type)) { + steps.push({type: 'key', key}); + if ('function' === typeof variable) { + steps.push({type: 'invoke', args: []}); + } + } + if (!variable || !variable.contextDescription) { + return steps; + } + const {children} = variable.contextDescription(); + const sublists = Object.entries(children) + .map(([key, description]) => ( + flatten(variableStepsList(key, variable[key], description, type)) + )) + .map((childLists) => ( + childLists.length > 0 ? [{type: 'key', key}].concat(childLists) : [] + )) + .filter((lists) => lists.length > 0) + return sublists.length > 0 ? sublists : [steps]; +}; + +export const contextStepsList = (context, type) => ( + flatten( + Object.entries(context.all()) + .map(([key, variable]) => ( + variableStepsList(key, variable, {type: typeFromV(variable)}, type) + )) + ) +); diff --git a/src/client/components/types/steps.jsx b/src/client/components/types/steps.jsx new file mode 100644 index 0000000..87c78b7 --- /dev/null +++ b/src/client/components/types/steps.jsx @@ -0,0 +1,95 @@ +import {compose} from '@avocado/core'; +import contempo from 'contempo'; +import React, {useState} from 'react'; + +import {contextStepsList} from './steps-lists'; +import {typeFits, typeFromSteps, typeFromV} from './typing'; +import Value from './value.type-renderer'; + +const decorate = compose( + contempo(require('./steps.raw.scss')), +); + +const Steps = (props) => { + const {context, steps, type, value} = props; + let stepsList; + if ('undefined' === typeof value) { + stepsList = contextStepsList(context, type); + } + else { + stepsList = contextStepsList( + context, + value.steps ? typeFromSteps(context, value.steps) : typeFromV(value.value), + ); + } + return ( +
    + {steps.map((step, i) => { + switch (step.type) { + case 'key': + const tierOptions = Object.keys(stepsList.reduce((r, optionSteps) => { + if (!optionSteps[i] || !optionSteps[i].key) { + return r; + } + for (let j = 0; j < i; ++j) { + if (steps[j].key !== optionSteps[j].key) { + return r; + } + } + return {...r, [optionSteps[i].key]: true}; + }, {})); + if (0 === i) { + const index = tierOptions.indexOf('context'); + if (-1 !== index) { + tierOptions.splice(index, 1); + } + tierOptions.push('Literal'); + } + return ( + + + + ); + case 'invoke': + return ( +
    + ( + {step.args.map((arg) => ( +
    + +
    + ))} + ) +
    + ); + default: + return null; + } + })} + { + value && ( + + = + + + ) + } +
    + ); +}; + +export default decorate(Steps); diff --git a/src/client/components/types/steps.raw.scss b/src/client/components/types/steps.raw.scss new file mode 100644 index 0000000..33e910a --- /dev/null +++ b/src/client/components/types/steps.raw.scss @@ -0,0 +1,53 @@ +:scope { + display: flex; + flex-wrap: wrap; +} + +.key + .key { + select { + border-left: none; + } +} + +.args { + margin-left: 2em; +} + +.arg { + border: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + margin-left: 2em; + padding: 0.5em; + &:not(:last-of-type) { + border-bottom: none; + } + &:nth-of-type(2n) { + background-color: rgba(0, 0, 0, 0.025); + } + &:nth-of-type(2n+1) { + background-color: rgba(255, 255, 255, 0.025); + } +} + +.arg .steps { + display: flex; +} + +.bracket, .paren { + color: #999999; +} + +.invoke { + // border-left: 1px dotted rgba(255, 255, 255, 0.1); + padding-left: 1em; +} + +.assign { + display: flex; + align-items: center; +} + +.op { + align-self: flex-start; + margin: 0.5em 1em; +} diff --git a/src/client/components/types/typing.js b/src/client/components/types/typing.js new file mode 100644 index 0000000..d44ebdd --- /dev/null +++ b/src/client/components/types/typing.js @@ -0,0 +1,61 @@ +export function typeFromV(v) { + if ('undefined' === typeof v) { + return 'undefined'; + } + if (null === v) { + return 'null'; + } + if ('number' === typeof v) { + return 'number'; + } + if ('string' === typeof v) { + return 'string'; + } + if ('boolean' === typeof v) { + return 'bool'; + } + if (v.length && 2 === v.length && v instanceof Array) { + return 'vector'; + } + if (!v.contextDescription) { + return 'object'; + } + const {type} = v.contextDescription(); + return type; +} + +export function typeFromSteps(context, steps) { + if (!steps || 0 === steps.length) { + return 'undefined'; + } + const first = context.map.get(steps[0].key); + const [, type] = steps.slice(1).reduce( + ([v, type], step, i) => { + if (i === steps.length - 2) { + if (v.contextDescription) { + const {children} = v.contextDescription(); + return [undefined, step.key ? children[step.key].type : type]; + } + else { + return [undefined, type]; + } + } + else { + if (step.key) { + const {children} = v.contextDescription(); + return [v[step.key], children[step.key].type]; + } + else { + return [v, type]; + } + } + step + }, + [first, typeFromV(first)], + ); + return type; +} + +export function typeFits(reference, candidate) { + return 'any' === reference || -1 !== (reference.split('|') || []).indexOf(candidate); +} diff --git a/src/client/components/types/value.raw.scss b/src/client/components/types/value.raw.scss index e69de29..3dec98d 100644 --- a/src/client/components/types/value.raw.scss +++ b/src/client/components/types/value.raw.scss @@ -0,0 +1,22 @@ +.fn { + text-align-last: right; +} + +:scope { + display: flex; + align-items: center; +} + +.object { + background-color: #333333; + border: 1px solid rgba(255, 255, 255, 0.1); + padding: 0.25em; +} + +.object, input { + // border-left: none; +} + +select { + margin-right: 0.25em; +} diff --git a/src/client/components/types/value.type-renderer.jsx b/src/client/components/types/value.type-renderer.jsx index db85dd3..b828974 100644 --- a/src/client/components/types/value.type-renderer.jsx +++ b/src/client/components/types/value.type-renderer.jsx @@ -4,77 +4,63 @@ import contempo from 'contempo'; import PropTypes from 'prop-types'; import React from 'react'; +import Bool from './bool.type-renderer'; import propertyPropTypes from './property-prop-types'; +import Steps from './steps'; +import {contextStepsList} from './steps-lists'; +import {typeFits, typeFromSteps, typeFromV} from './typing'; -const reduceStep = (r, step) => { - switch (step.type) { - case 'key': - return `${r}.${step.key}`; - case 'invoke': - return ( - - - ( -
    - { - step.args.length > 0 && ( - step.args.slice(1).map(renderValue).reduce( - (r2, element, i) => ( - <> - {r2} -
    - {element} -
    - - ), -
    - {renderValue(step.args[0])} -
    , - ) - ) - } -
    - ) -
    - ); - default: - return undefined; - } -}; - -const renderValue = (value) => { +const renderValue = (context, type, value) => { switch (value.type) { case 'traversal': { - const {steps} = value; - return ( - - {steps.length > 0 && ( - steps.slice(1).reduce( - reduceStep, - steps[0].key, - ) - )} - - ); + return ; } case 'literal': - if ( - null === value.value - || 'string' === typeof value.value - || 'number' === typeof value.value - ) { - return ; - } + // const type = typeFromV(value.value); + const stepsList = contextStepsList(context, type); + const tierOptions = Object.keys(stepsList.reduce((r, optionSteps) => { + if (!optionSteps[0] || !optionSteps[0].key) { + return r; + } + return {...r, [optionSteps[0].key]: true}; + }, {})); + tierOptions.splice(tierOptions.indexOf('context'), 1); + tierOptions.push('Literal'); return ( -
    - {'{'} -
    -            
    -              {'  '}
    -              {JSON.stringify(value.value, null, 2).slice(1).slice(0, -1).trim('\n')}
    -            
    -          
    - {'}'} +
    + + { + (() => { + switch (type) { + case 'bool': + return ; + case 'number': + case 'string': + return ; + case 'object': + return ( +
    + {'{'} +
    +                        
    +                          {'  '}
    +                          {JSON.stringify(value.value, null, 2).slice(1).slice(0, -1).trim('\n')}
    +                        
    +                      
    + {'}'} +
    + ); + default: + return null; + } + })() + }
    ); default: @@ -87,11 +73,13 @@ const decorate = compose( ); const Value = ({ + context, name, label, options, + type, value, -}) => renderValue(value); +}) => renderValue(context, type || 'any', value); Value.propTypes = { ...propertyPropTypes, diff --git a/src/client/index.scss b/src/client/index.scss index eae2426..787ad04 100644 --- a/src/client/index.scss +++ b/src/client/index.scss @@ -72,7 +72,7 @@ code { label { align-items: left; - background-color: #272727; + background-color: rgba(255, 255, 255, 0.025); color: #ffffff; display: flex; flex-direction: column; @@ -93,12 +93,16 @@ label { } label:nth-of-type(2n+1) { - background-color: #1d1d1d + background-color: rgba(0, 0, 0, 0.025); +} + +[contenteditable] { + cursor: text; } input { background: #333; - border: 1px solid black; + border: 1px solid rgba(255, 255, 255, 0.1); color: #ffffff; font-size: 0.75em; padding: 0.5em; @@ -106,7 +110,7 @@ input { fieldset { background-color: #151515; - border: 1px solid #000000; + border: 1px solid rgba(255, 255, 255, 0.1); display: inline-block; margin: 0 0 1em 0; padding: 0.5em; @@ -121,16 +125,19 @@ button, input[type="checkbox"], input[type="checkbox"] + label { select { background: #222222; - border: 1px solid #000000; + border: 1px solid rgba(255, 255, 255, 0.1); color: #ffffff; + cursor: pointer; font-size: 0.75em; - line-height: 1em; + margin: 0.25em 0; -moz-appearance: none; -webkit-appearance: none; padding: 0.5em; } *:focus { + position: relative; box-shadow: 0 0 2px 0 #d68030ff; outline: none; + z-index: 1; }