Merge pull request #253 from Budibase/property-panel/master

Complete CSS State Styles and Property Panel Structure
This commit is contained in:
Conor_Mack 2020-05-26 14:32:49 +01:00 committed by GitHub
commit b4c1468889
36 changed files with 1379 additions and 1066 deletions

View File

@ -79,7 +79,8 @@
"rollup-plugin-svelte": "^5.0.3",
"rollup-plugin-terser": "^4.0.4",
"rollup-plugin-url": "^2.2.2",
"svelte": "^3.0.0"
"svelte": "^3.0.0",
"svelte-color-picker": "^1.0.7"
},
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
}

View File

@ -1,142 +1,54 @@
import { pipe } from "components/common/core"
import { filter, map, reduce, toPairs } from "lodash/fp"
const self = n => n
const join_with = delimiter => a => a.join(delimiter)
const empty_string_to_unset = s => (s.length ? s : "0")
const add_suffix_if_number = suffix => s => {
try {
if (isNaN(s) || isNaN(parseFloat(s))) return s
} catch (_) {
return s
}
return s + suffix
}
export const make_margin = values =>
pipe(values, [
map(empty_string_to_unset),
map(add_suffix_if_number("px")),
join_with(" "),
])
const css_map = {
templaterows: {
name: "grid-template-rows",
generate: self,
},
templatecolumns: {
name: "grid-template-columns",
generate: self,
},
align: {
name: "align-items",
generate: self,
},
justify: {
name: "justify-content",
generate: self,
},
direction: {
name: "flex-direction",
generate: self,
},
gridarea: {
name: "grid-area",
generate: make_margin,
},
gap: {
name: "grid-gap",
generate: n => `${n}px`,
},
columnstart: {
name: "grid-column-start",
generate: self,
},
columnend: {
name: "grid-column-end",
generate: self,
},
rowstart: {
name: "grid-row-start",
generate: self,
},
rowend: {
name: "grid-row-end",
generate: self,
},
padding: {
name: "padding",
generate: make_margin,
},
margin: {
name: "margin",
generate: make_margin,
},
zindex: {
name: "z-index",
generate: self,
},
height: {
name: "height",
generate: self,
},
width: {
name: "width",
generate: self,
},
}
export const generate_rule = ([name, values]) =>
`${css_map[name].name}: ${css_map[name].generate(values)};`
const handle_grid = (acc, [name, value]) => {
let tmp = []
if (name === "row" || name === "column") {
if (value[0]) tmp.push([`${name}start`, value[0]])
if (value[1]) tmp.push([`${name}end`, value[1]])
return acc.concat(tmp)
}
return acc.concat([[name, value]])
}
const object_to_css_string = [
toPairs,
reduce(handle_grid, []),
filter(v => (Array.isArray(v[1]) ? v[1].some(s => s.length) : v[1].length)),
map(generate_rule),
join_with("\n"),
]
export const generate_css = ({ layout, position }) => {
let _layout = pipe(layout, object_to_css_string)
if (_layout.length) {
_layout += `\ndisplay: ${_layout.includes("flex") ? "flex" : "grid"};`
}
return {
layout: _layout,
position: pipe(position, object_to_css_string),
}
}
const apply_class = (id, name, styles) => `.${name}-${id} {\n${styles}\n}`
export const generate_screen_css = component_array => {
export const generate_screen_css = component_arr => {
let styles = ""
let emptyStyles = { layout: {}, position: {} }
for (let i = 0; i < component_array.length; i += 1) {
const { _styles, _id, _children } = component_array[i]
const { layout, position } = generate_css(_styles || emptyStyles)
styles += apply_class(_id, "pos", position) + "\n"
styles += apply_class(_id, "lay", layout) + "\n"
for (const { _styles, _id, _children, _component } of component_arr) {
let [componentName] = _component.match(/[a-z]*$/)
Object.keys(_styles).forEach(selector => {
const cssString = generate_css(_styles[selector])
if (cssString) {
styles += apply_class(_id, componentName, cssString, selector)
}
})
if (_children && _children.length) {
styles += generate_screen_css(_children) + "\n"
}
}
return styles.trim()
}
export const generate_css = style => {
let cssString = Object.entries(style).reduce((str, [key, value]) => {
//TODO Handle arrays and objects here also
if (typeof value === "string") {
if (value) {
return (str += `${key}: ${value};\n`)
}
} else if (Array.isArray(value)) {
if (value.length > 0 && !value.every(v => v === "")) {
return (str += `${key}: ${value
.map(generate_array_styles)
.join(" ")};\n`)
}
}
}, "")
return (cssString || "").trim()
}
export const generate_array_styles = item => {
let safeItem = item === "" ? 0 : item
let hasPx = new RegExp("px$")
if (!hasPx.test(safeItem)) {
return `${safeItem}px`
} else {
return safeItem
}
}
export const apply_class = (id, name = "element", styles, selector) => {
if (selector === "normal") {
return `.${name}-${id} {\n${styles}\n}`
} else {
let sel = selector === "selected" ? "::selection" : `:${selector}`
return `.${name}-${id}${sel} {\n${styles}\n}`
}
}

View File

@ -140,10 +140,11 @@ const _saveScreen = async (store, s, screen) => {
return s
}
const _saveScreenApi = (screen, s) =>
const _saveScreenApi = (screen, s) => {
api
.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}/screen`, screen)
.then(() => _savePage(s))
}
const createScreen = store => (screenName, route, layoutComponentName) => {
store.update(state => {
@ -278,7 +279,6 @@ const removeStylesheet = store => stylesheet => {
const _savePage = async s => {
const page = s.pages[s.currentPageName]
await api.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}`, {
page: { componentLibraries: s.pages.componentLibraries, ...page },
uiFunctions: s.currentPageFunctions,
@ -427,6 +427,7 @@ const setComponentStyle = store => (type, name, value) => {
state.currentComponentInfo._styles = {}
}
state.currentComponentInfo._styles[type][name] = value
state.currentPreviewItem._css = generate_screen_css([
state.currentPreviewItem.props,
])

View File

@ -1,13 +1,78 @@
<script>
import { onMount, beforeUpdate, afterUpdate } from "svelte"
import { onMount } from "svelte"
import { HsvPicker } from "svelte-color-picker"
export let value = null
export let onChanged = () => {}
export let swatches = []
// export let initialValue = "#ffffff"
export let onChange = color => {}
export let open = false
let value = "#ffffff"
let picker
let cp = null
let _justMounted = true //see onColorChange
let pickerHeight = 275
let colorPreview
let pickerTopPosition = null
function rbgaToHexa({ r, g, b, a }) {
r = r.toString(16)
g = g.toString(16)
b = b.toString(16)
a = Math.round(a * 255).toString(16)
if (r.length == 1) r = "0" + r
if (g.length == 1) g = "0" + g
if (b.length == 1) b = "0" + b
if (a.length == 1) a = "0" + a
return "#" + r + g + b + a
}
function onColourChange(rgba) {
value = rbgaToHexa(rgba.detail)
//Hack: so that color change doesn't fire onMount
if (!_justMounted) {
// onChange(value)
}
_justMounted = false
}
function toggleColorpicker(isOpen) {
if (isOpen) {
const {
y: previewYPosition,
height: previewHeight,
} = colorPreview.getBoundingClientRect()
let wiggleRoom = window.innerHeight - previewYPosition
let displayTop = wiggleRoom < pickerHeight
if (displayTop) {
pickerTopPosition = previewYPosition - (pickerHeight - window.scrollY)
} else {
pickerTopPosition = null
}
}
open = isOpen
}
$: style = open ? "display: block;" : "display: none;"
$: pickerStyle = pickerTopPosition ? `top: ${pickerTopPosition}px;` : ""
</script>
<div
bind:this={colorPreview}
on:click={() => toggleColorpicker(true)}
class="color-preview"
style={`background: ${value}`} />
<div class="colorpicker" {style}>
<div class="overlay" on:click|self={() => toggleColorpicker(false)} />
<div class="cp" style={pickerStyle}>
<HsvPicker on:colorChange={onColourChange} startColor={value} />
</div>
</div>
<!--
OLD LOCAL STORAGE OPTIONS. INCLUDING FOR ADDING LATER
function getRecentColors() {
let colorStore = localStorage.getItem("bb:recentColors")
if (!!colorStore) {
@ -25,47 +90,27 @@
picker.addSwatch(color)
localStorage.setItem("bb:recentColors", JSON.stringify(swatches))
}
} -->
<style>
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
/* background: rgba(5, 5, 5, 0.25); */
}
function createPicker() {
picker = Pickr.create({
el: cp,
theme: "nano",
default: value || "#000000",
swatches,
closeWithKey: "Escape",
components: {
preview: true,
opacity: true,
hue: true,
interaction: {
hex: true,
rgba: true,
input: true,
save: true,
},
},
})
.cp {
position: absolute;
right: 25px;
}
afterUpdate(() => {
picker.setColor(value)
})
onMount(() => {
getRecentColors()
createPicker()
picker.on("save", (colour, instance) => {
let color = colour.toHEXA().toString()
onChanged(color)
setRecentColor(color)
picker.hide()
})
})
</script>
<div bind:this={cp} class="color-picker" />
.color-preview {
height: 30px;
width: 100%;
margin: 5px;
cursor: pointer;
border: 1px solid gainsboro;
}
</style>

View File

@ -1,8 +1,11 @@
<script>
export let value = ""
export let width = ""
let style = { width }
</script>
<input type="text" on:change bind:value />
<input type="text" style={`width: ${width};`} on:change bind:value />
<style>
input {

View File

@ -2,36 +2,50 @@
import { onMount } from "svelte"
export let meta = []
export let size = ""
export let values = []
export let label = ""
export let value = [0, 0, 0, 0]
export let type = "number"
export let onStyleChanged = () => {}
export let onChange = () => {}
let _values = values.map(v => v)
$: onStyleChanged(_values)
function handleChange(val, idx) {
value.splice(idx, 1, val)
value = value
onChange(value)
}
</script>
<div class="inputs {size}">
<div class="input-container">
<div class="label">{label}</div>
<div class="inputs">
{#each meta as { placeholder }, i}
<input
{type}
{placeholder}
value={values[i]}
on:input={e => (_values[i] = e.target.value)} />
placeholder={placeholder || ''}
value={!value || value[i] === 0 ? '' : value[i]}
on:change={e => handleChange(e.target.value || 0, i)} />
{/each}
</div>
</div>
<style>
.inputs {
.input-container {
display: flex;
justify-content: space-between;
}
.label {
flex: 0;
}
.inputs {
flex: 1;
}
input {
width: 83px;
width: 40px;
height: 32px;
font-size: 13px;
font-weight: 700;
margin: 0px 5px;
color: #163057;
opacity: 0.7;
padding: 5px 10px;
@ -49,17 +63,11 @@
margin: 0;
}
.small > input {
width: 38px;
height: 38px;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 0;
input[type="number"] {
-moz-appearance: textfield;
}
.small > input::placeholder {
input::placeholder {
text-align: center;
}
</style>

View File

@ -16,6 +16,11 @@
return props
}
const getComponentTypeName = component => {
let [componentName] = component._component.match(/[a-z]*$/)
return componentName || "element"
}
$: iframe &&
console.log(
iframe.contentDocument.head.insertAdjacentHTML(
@ -60,7 +65,7 @@
_children: [
{
_component: "@budibase/standard-components/container",
_styles: { position: {}, layout: {} },
_styles: { normal: {}, hover: {}, active: {}, selected: {} },
_id: "__screenslot__text",
_code: "",
className: "",
@ -69,7 +74,12 @@
_children: [
{
_component: "@budibase/standard-components/text",
_styles: { position: {}, layout: {} },
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_id: "__screenslot__text_2",
_code: "",
text: "content",
@ -88,6 +98,8 @@
appRootPath: "",
}
$: selectedComponentType = getComponentTypeName($store.currentComponentInfo)
$: selectedComponentId = $store.currentComponentInfo
? $store.currentComponentInfo._id
: ""
@ -102,6 +114,7 @@
srcdoc={iframeTemplate({
styles,
stylesheetLinks,
selectedComponentType,
selectedComponentId,
frontendDefinition: JSON.stringify(frontendDefinition),
currentPageFunctions: $store.currentPageFunctions,

View File

@ -1,6 +1,7 @@
export default ({
styles,
stylesheetLinks,
selectedComponentType,
selectedComponentId,
frontendDefinition,
currentPageFunctions,
@ -11,7 +12,7 @@ export default ({
<style>
${styles || ""}
.pos-${selectedComponentId} {
.${selectedComponentType}-${selectedComponentId} {
border: 2px solid #0055ff;
}

View File

@ -0,0 +1,43 @@
<script>
export let categories = []
export let selectedCategory = {}
export let onClick = category => {}
</script>
<ul class="tabs">
{#each categories as category}
<li
on:click={() => onClick(category)}
class:active={selectedCategory === category}>
{category.name}
</li>
{/each}
</ul>
<style>
.tabs {
display: flex;
justify-content: center;
list-style: none;
margin: 0 auto;
padding: 0 30px;
border-bottom: 1px solid #d8d8d8;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.14px;
}
li {
color: #808192;
margin: 0 5px;
padding: 0 8px;
cursor: pointer;
}
.active {
border-bottom: solid 3px #0055ff;
color: #393c44;
}
</style>

View File

@ -1,4 +1,5 @@
<script>
import { setContext, onMount } from "svelte"
import PropsView from "./PropsView.svelte"
import { store } from "builderStore"
import IconButton from "components/common/IconButton.svelte"
@ -13,23 +14,63 @@
import LayoutEditor from "./LayoutEditor.svelte"
import EventsEditor from "./EventsEditor"
let current_view = "props"
let codeEditor
import panelStructure from "./temporaryPanelStructure.js"
import CategoryTab from "./CategoryTab.svelte"
import DesignView from "./DesignView.svelte"
import SettingsView from "./SettingsView.svelte"
let current_view = "design"
let codeEditor
let flattenedPanel = flattenComponents(panelStructure.categories)
let categories = [
{ value: "design", name: "Design" },
{ value: "settings", name: "Settings" },
{ value: "actions", name: "Actions" },
]
let selectedCategory = categories[0]
$: component = $store.currentComponentInfo
$: originalName = component.name
$: name =
$store.currentView === "detail"
? $store.currentPreviewItem.name
: component._component
$: description = component.description
$: components = $store.components
$: componentInstance = $store.currentComponentInfo
$: componentDefinition = $store.components[componentInstance._component]
$: componentPropDefinition =
flattenedPanel.find(
//use for getting controls for each component property
c => c._component === componentInstance._component
) || {}
$: panelDefinition = componentPropDefinition.properties
? componentPropDefinition.properties[selectedCategory.value]
: {}
// SCREEN PROPS =============================================
$: screen_props =
$store.currentFrontEndType === "page"
? getProps($store.currentPreviewItem, ["name", "favicon"])
: getProps($store.currentPreviewItem, ["name", "description", "route"])
const onStyleChanged = store.setComponentStyle
const onPropChanged = store.setComponentProp
function walkProps(component, action) {
action(component)
if (component.children) {
for (let child of component.children) {
walkProps(child, action)
}
}
}
function flattenComponents(props) {
const components = []
props.forEach(comp =>
walkProps(comp, c => {
if ("_component" in c) {
components.push(c)
}
})
)
return components
}
function getProps(obj, keys) {
return keys.map((key, i) => [key, obj[key], obj.props._id + i])
@ -37,116 +78,33 @@
</script>
<div class="root">
<ul>
<li>
<button
class:selected={current_view === 'props'}
on:click={() => (current_view = 'props')}>
<PaintIcon />
</button>
</li>
<li>
<button
class:selected={current_view === 'layout'}
on:click={() => (current_view = 'layout')}>
<LayoutIcon />
</button>
</li>
{#if !component._component.startsWith('##')}
<li>
<button
class:selected={current_view === 'code'}
on:click={() => codeEditor && codeEditor.show()}>
{#if component._code && component._code.trim().length > 0}
<div class="button-indicator">
<CircleIndicator />
</div>
{/if}
<TerminalIcon />
</button>
</li>
<li>
<button
class:selected={current_view === 'events'}
on:click={() => (current_view = 'events')}>
<EventsIcon />
</button>
</li>
{/if}
</ul>
<CategoryTab
onClick={category => (selectedCategory = category)}
{categories}
{selectedCategory} />
<div class="component-props-container">
{#if current_view === 'props'}
{#if $store.currentView === 'detail'}
{#each screen_props as [k, v, id] (id)}
<div class="detail-prop" for={k}>
<label>{k}:</label>
<input
id={k}
value={v}
on:input={({ target }) => store.setMetadataProp(k, target.value)} />
</div>
{/each}
<PropsView {component} {components} />
{:else}
<PropsView {component} {components} />
{#if selectedCategory.value === 'design'}
<DesignView {panelDefinition} {componentInstance} {onStyleChanged} />
{:else if selectedCategory.value === 'settings'}
<SettingsView
{componentInstance}
{componentDefinition}
{panelDefinition}
onChange={onPropChanged} />
{/if}
{:else if current_view === 'layout'}
<LayoutEditor {onStyleChanged} {component} />
{:else if current_view === 'events'}
<EventsEditor {component} {components} />
{/if}
<CodeEditor
bind:this={codeEditor}
code={component._code}
onCodeChanged={store.setComponentCode} />
</div>
</div>
<style>
.detail-prop {
height: 40px;
margin-bottom: 15px;
display: grid;
grid-template-rows: 1fr;
grid-template-columns: 70px 1fr;
grid-gap: 10px;
}
.detail-prop label {
word-wrap: break-word;
font-size: 13px;
font-weight: 700;
color: #163057;
opacity: 0.6;
padding-top: 13px;
margin-bottom: 0;
}
input {
height: 30px;
padding-left: 8px;
padding-right: 8px;
border: 1px solid #dbdbdb;
border-radius: 2px;
opacity: 0.5;
}
input:focus {
outline: 0;
background-color: #fff;
color: #666;
border-color: #1e87f0;
}
.root {
height: 100%;
padding: 20px;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
.title > div:nth-child(1) {
@ -162,52 +120,4 @@
margin-top: 10px;
flex: 1 1 auto;
}
ul {
list-style: none;
display: flex;
justify-content: space-between;
padding: 0;
}
li {
background: none;
border-radius: 3px;
width: 48px;
height: 48px;
}
li button {
width: 48px;
height: 48px;
background: none;
border: none;
border-radius: 3px;
padding: 7px;
outline: none;
cursor: pointer;
position: relative;
}
li:nth-last-child(1) {
margin-right: 0px;
background: none;
border-radius: 3px;
width: 48px;
height: 48px;
}
.selected {
color: var(--button-text);
background: #f9f9f9 !important;
width: 48px;
height: 48px;
}
.button-indicator {
position: absolute;
top: 8px;
right: 10px;
color: var(--button-text);
}
</style>

View File

@ -2,6 +2,7 @@
import { splitName } from "./pagesParsing/splitRootComponentName.js"
import components from "./temporaryPanelStructure.js"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import CategoryTab from "./CategoryTab.svelte"
import {
find,
sortBy,
@ -36,15 +37,12 @@
</script>
<div class="root">
<ul class="tabs">
{#each categories as category}
<li
on:click={() => (selectedCategory = category)}
class:active={selectedCategory === category}>
{category.name}
</li>
{/each}
</ul>
<CategoryTab
onClick={category => (selectedCategory = category)}
{selectedCategory}
{categories} />
<div class="panel">
<Tab
list={selectedCategory}

View File

@ -30,7 +30,7 @@
<button
class:selected={selected === PROPERTIES_TAB}
on:click={() => selectTab(PROPERTIES_TAB)}>
Properties
Edit
</button>
</div>
@ -50,6 +50,14 @@
</div>
<style>
.root {
height: 100%;
display: flex;
flex-direction: column;
padding: 20px 0;
border-left: solid 1px #e8e8ef;
}
.switcher {
display: flex;
margin: 0px 20px 20px 20px;

View File

@ -0,0 +1,71 @@
<script>
import PropertyGroup from "./PropertyGroup.svelte"
import FlatButtonGroup from "./FlatButtonGroup.svelte"
export let panelDefinition = {}
export let componentInstance = {}
export let componentDefinition = {}
export let onStyleChanged = () => {}
let selectedCategory = "normal"
const getProperties = name => panelDefinition[name]
function onChange(category) {
selectedCategory = category
}
const buttonProps = [
{ value: "normal", text: "Normal" },
{ value: "hover", text: "Hover" },
{ value: "active", text: "Active" },
{ value: "selected", text: "Selected" },
]
$: propertyGroupNames = Object.keys(panelDefinition)
</script>
<div class="design-view-container">
<div class="design-view-state-categories">
<FlatButtonGroup value={selectedCategory} {buttonProps} {onChange} />
</div>
<div class="design-view-property-groups">
{#if propertyGroupNames.length > 0}
{#each propertyGroupNames as groupName}
<PropertyGroup
name={groupName}
properties={getProperties(groupName)}
styleCategory={selectedCategory}
{onStyleChanged}
{componentDefinition}
{componentInstance} />
{/each}
{:else}
<div class="no-design">
<span>This component does not have any design properties</span>
</div>
{/if}
</div>
</div>
<style>
.design-view-container {
display: flex;
flex-direction: column;
width: 100%;
}
.design-view-state-categories {
flex: 0 0 50px;
}
.design-view-property-groups {
flex: 1;
}
.no-design {
text-align: center;
}
</style>

View File

@ -0,0 +1,38 @@
<script>
export let value = ""
export let text = ""
export let icon = ""
export let onClick = value => {}
export let selected = false
$: useIcon = !!icon
</script>
<div class="flatbutton" class:selected on:click={() => onClick(value || text)}>
{#if useIcon}
<i class={icon} />
{:else}
<span>{text}</span>
{/if}
</div>
<style>
.flatbutton {
cursor: pointer;
padding: 5px;
text-align: center;
background: #ffffff;
color: #808192;
border-radius: 4px;
font-family: Roboto;
font-size: 11px;
font-weight: 500;
letter-spacing: 0.11px;
transition: background 0.5s, color 0.5s ease;
}
.selected {
background: #808192;
color: #ffffff;
}
</style>

View File

@ -0,0 +1,54 @@
<script>
import { onMount } from "svelte"
import FlatButton from "./FlatButton.svelte"
export let buttonProps = []
export let isMultiSelect = false
export let value = []
export let initialValue = ""
export let onChange = selected => {}
onMount(() => {
if (!value && !!initialValue) {
value = initialValue
}
})
function onButtonClicked(v) {
let val
if (isMultiSelect) {
if (value.includes(v)) {
let idx = value.findIndex(i => i === v)
val = [...value].splice(idx, 1)
} else {
val = [...value, v]
}
} else {
val = v
}
onChange(val)
}
</script>
<div class="flatbutton-group">
{#each buttonProps as props}
<div class="button-container">
<FlatButton
selected={value.includes(props.value)}
onClick={onButtonClicked}
{...props} />
</div>
{/each}
</div>
<style>
.flatbutton-group {
display: flex;
flex-flow: row nowrap;
}
.button-container {
flex: 1;
margin: 5px;
}
</style>

View File

@ -0,0 +1,36 @@
<script>
import { onMount } from "svelte"
export let value = ""
export let onChange = value => {}
export let options = []
export let initialValue = ""
export let styleBindingProperty = ""
const handleStyleBind = value =>
!!styleBindingProperty ? { style: `${styleBindingProperty}: ${value}` } : {}
$: isOptionsObject = options.every(o => typeof o === "object")
onMount(() => {
if (!value && !!initialValue) {
value = initialValue
}
})
</script>
<select
class="uk-select uk-form-small"
{value}
on:change={ev => onChange(ev.target.value)}>
{#if isOptionsObject}
{#each options as { value, label }}
<option {...handleStyleBind(value || label)} value={value || label}>
{label}
</option>
{/each}
{:else}
{#each options as value}
<option {...handleStyleBind(value)} {value}>{value}</option>
{/each}
{/if}
</select>

View File

@ -0,0 +1,63 @@
<script>
import { onMount, getContext } from "svelte"
export let label = ""
export let control = null
export let key = ""
export let value
export let props = {}
export let onChange = () => {}
function handleChange(key, v) {
if (v.target) {
let val = props.valueKey ? v.target[props.valueKey] : v.target.value
onChange(key, val)
} else {
onChange(key, v)
}
}
const safeValue = () => {
return value === undefined && props.defaultValue !== undefined
? props.defaultValue
: value
}
//Incase the component has a different value key name
const handlevalueKey = value =>
props.valueKey ? { [props.valueKey]: safeValue() } : { value: safeValue() }
</script>
<div class="property-control">
<div class="label">{label}</div>
<div class="control">
<svelte:component
this={control}
{...handlevalueKey(value)}
on:change={val => handleChange(key, val)}
onChange={val => handleChange(key, val)}
{...props} />
</div>
</div>
<style>
.property-control {
display: flex;
flex-flow: row nowrap;
margin: 8px 0px;
}
.label {
flex: 0 0 50px;
padding: 0px 5px;
font-size: 12px;
font-weight: 500;
letter-spacing: 0.12px;
text-align: left;
}
.control {
flex: 1;
padding-left: 5px;
}
</style>

View File

@ -0,0 +1,81 @@
<script>
import { excludeProps } from "./propertyCategories.js"
import PropertyControl from "./PropertyControl.svelte"
export let name = ""
export let styleCategory = "normal"
export let properties = []
export let componentInstance = {}
export let onStyleChanged = () => {}
export let show = false
const capitalize = name => name[0].toUpperCase() + name.slice(1)
$: icon = show ? "ri-arrow-down-s-fill" : "ri-arrow-right-s-fill"
$: style = componentInstance["_styles"][styleCategory] || {}
</script>
<div class="property-group-container">
<div class="property-group-name" on:click={() => (show = !show)}>
<div class="icon">
<i class={icon} />
</div>
<div class="name">{capitalize(name)}</div>
</div>
<div class="property-panel" class:show>
{#each properties as props}
<PropertyControl
label={props.label}
control={props.control}
key={props.key}
value={style[props.key]}
onChange={(key, value) => onStyleChanged(styleCategory, key, value)}
props={{ ...excludeProps(props, ['control', 'label']) }} />
{/each}
</div>
</div>
<style>
.property-group-container {
display: flex;
flex-direction: column;
height: auto;
background: #fbfbfb;
margin: 5px;
padding: 5px;
}
.property-group-name {
cursor: pointer;
flex: 0 0 20px;
display: flex;
flex-flow: row nowrap;
}
.name {
flex: 1;
text-align: left;
padding-top: 2px;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.14px;
color: #393c44;
}
.icon {
flex: 0 0 20px;
text-align: center;
}
.property-panel {
height: 0px;
overflow: hidden;
}
.show {
overflow: auto;
height: auto;
}
</style>

View File

@ -0,0 +1,41 @@
<script>
import PropertyControl from "./PropertyControl.svelte"
import InputGroup from "../common/Inputs/InputGroup.svelte"
import Colorpicker from "../common/Colorpicker.svelte"
import { excludeProps } from "./propertyCategories.js"
export let panelDefinition = []
export let componentDefinition = {}
export let componentInstance = {}
export let onChange = () => {}
const propExistsOnComponentDef = prop => prop in componentDefinition.props
function handleChange(key, data) {
data.target ? onChange(key, data.target.value) : onChange(key, data)
}
</script>
{#if panelDefinition.length > 0}
{#each panelDefinition as definition}
{#if propExistsOnComponentDef(definition.key)}
<PropertyControl
control={definition.control}
label={definition.label}
key={definition.key}
value={componentInstance[definition.key]}
{onChange}
props={{ ...excludeProps(definition, ['control', 'label']) }} />
{/if}
{/each}
{:else}
<div>
<span>This component does not have any settings.</span>
</div>
{/if}
<style>
div {
text-align: center;
}
</style>

View File

@ -0,0 +1,240 @@
<script>
import ComponentsHierarchy from "./ComponentsHierarchy.svelte"
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
import PageLayout from "./PageLayout.svelte"
import PagesList from "./PagesList.svelte"
import { store } from "builderStore"
import IconButton from "components/common/IconButton.svelte"
import NewScreen from "./NewScreen.svelte"
import CurrentItemPreview from "./CurrentItemPreview.svelte"
import SettingsView from "./SettingsView.svelte"
import PageView from "./PageView.svelte"
import ComponentsPaneSwitcher from "./ComponentsPaneSwitcher.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { last } from "lodash/fp"
import { AddIcon } from "components/common/Icons"
let newScreenPicker
let confirmDeleteDialog
let componentToDelete = ""
const newScreen = () => {
newScreenPicker.show()
}
let settingsView
const settings = () => {
settingsView.show()
}
const confirmDeleteComponent = component => {
componentToDelete = component
confirmDeleteDialog.show()
}
const lastPartOfName = c => (c ? last(c.split("/")) : "")
</script>
<div class="root">
<div class="ui-nav">
<div class="pages-list-container">
<div class="nav-header">
<span class="navigator-title">Navigator</span>
<div class="border-line" />
<span class="components-nav-page">Pages</span>
</div>
<div class="nav-items-container">
<PagesList />
</div>
</div>
<div class="border-line" />
<PageLayout layout={$store.pages[$store.currentPageName]} />
<div class="border-line" />
<div class="components-list-container">
<div class="nav-group-header">
<span class="components-nav-header" style="margin-top: 0;">
Screens
</span>
<div>
<button on:click={newScreen}>
<AddIcon />
</button>
</div>
</div>
<div class="nav-items-container">
<ComponentsHierarchy screens={$store.screens} />
</div>
</div>
</div>
<div class="preview-pane">
<CurrentItemPreview />
</div>
{#if $store.currentFrontEndType === 'screen' || $store.currentFrontEndType === 'page'}
<div class="components-pane">
<ComponentsPaneSwitcher />
</div>
{/if}
</div>
<NewScreen bind:this={newScreenPicker} />
<SettingsView bind:this={settingsView} />
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete)}' component`}
okText="Delete Component"
onOk={() => store.deleteComponent(componentToDelete)} />
<style>
button {
cursor: pointer;
outline: none;
border: none;
border-radius: 5px;
width: 20px;
padding-bottom: 10px;
display: flex;
justify-content: center;
align-items: center;
padding: 0;
}
.root {
display: grid;
grid-template-columns: 275px 1fr 300px;
height: 100%;
width: 100%;
background: #fafafa;
}
@media only screen and (min-width: 1800px) {
.root {
display: grid;
grid-template-columns: 300px 1fr 300px;
height: 100%;
width: 100%;
background: #fafafa;
}
}
.ui-nav {
grid-column: 1;
background-color: var(--white);
height: calc(100vh - 49px);
padding: 0;
overflow: scroll;
display: flex;
flex-direction: column;
}
.preview-pane {
grid-column: 2;
margin: 40px;
background: #fff;
border-radius: 5px;
box-shadow: 0 0px 6px rgba(0, 0, 0, 0.05);
}
.components-pane {
grid-column: 3;
background-color: var(--white);
height: 100vh;
overflow-y: scroll;
}
.components-nav-page {
font-size: 13px;
color: #000333;
text-transform: uppercase;
padding-left: 20px;
margin-top: 20px;
font-weight: 600;
opacity: 0.4;
letter-spacing: 1px;
}
.components-nav-header {
font-size: 13px;
color: #000333;
text-transform: uppercase;
margin-top: 20px;
font-weight: 600;
opacity: 0.4;
letter-spacing: 1px;
}
.nav-header {
display: flex;
flex-direction: column;
margin-top: 20px;
}
.nav-items-container {
padding: 1rem 0rem 0rem 0rem;
}
.nav-group-header {
display: flex;
padding: 0px 20px 0px 20px;
font-size: 0.9rem;
font-weight: bold;
justify-content: space-between;
align-items: center;
}
.nav-group-header > div:nth-child(1) {
padding: 0rem 0.5rem 0rem 0rem;
vertical-align: bottom;
grid-column-start: icon;
margin-right: 5px;
}
.nav-group-header > span:nth-child(3) {
margin-left: 5px;
vertical-align: bottom;
grid-column-start: title;
margin-top: auto;
}
.nav-group-header > div:nth-child(3) {
vertical-align: bottom;
grid-column-start: button;
cursor: pointer;
color: var(--primary75);
}
.nav-group-header > div:nth-child(3):hover {
color: var(--primary75);
}
.navigator-title {
font-size: 14px;
color: var(--secondary100);
font-weight: 600;
text-transform: uppercase;
padding: 0 20px 20px 20px;
line-height: 1rem !important;
letter-spacing: 1px;
}
.border-line {
border-bottom: 1px solid #d8d8d8;
}
.components-list-container {
padding: 20px 0px 0 0;
}
</style>

View File

@ -24,7 +24,7 @@ export const createProps = (componentDefinition, derivedFromProps) => {
const props = {
_id: uuid(),
_component: componentDefinition._component,
_styles: { position: {}, layout: {} },
_styles: { normal: {}, hover: {}, active: {}, selected: {} },
_code: "",
}
@ -71,7 +71,7 @@ export const makePropsSafe = (componentDefinition, props) => {
}
if (!props._styles) {
props._styles = { layout: {}, position: {} }
props._styles = { normal: {}, hover: {}, active: {}, selected: {} }
}
return props

View File

@ -0,0 +1,173 @@
import Input from "../common/Input.svelte"
import OptionSelect from "./OptionSelect.svelte"
import InputGroup from "../common/Inputs/InputGroup.svelte"
// import Colorpicker from "../common/Colorpicker.svelte"
/*
TODO: Allow for default values for all properties
*/
export const layout = [
{
label: "Direction",
key: "flex-direction",
control: OptionSelect,
initialValue: "columnReverse",
options: [
{ label: "row" },
{ label: "row-reverse", value: "rowReverse" },
{ label: "column" },
{ label: "column-reverse", value: "columnReverse" },
],
},
{ label: "Justify", key: "justify-content", control: Input },
{ label: "Align", key: "align-items", control: Input },
{
label: "Wrap",
key: "flex-wrap",
control: OptionSelect,
options: [{ label: "wrap" }, { label: "no wrap", value: "noWrap" }],
},
]
const spacingMeta = [
{ placeholder: "T" },
{ placeholder: "R" },
{ placeholder: "B" },
{ placeholder: "L" },
]
export const spacing = [
{
label: "Padding",
key: "padding",
control: InputGroup,
meta: spacingMeta,
},
{ label: "Margin", key: "margin", control: InputGroup, meta: spacingMeta },
]
export const size = [
{ label: "Width", key: "width", control: Input },
{ label: "Height", key: "height", control: Input },
{ label: "Min W", key: "min-width", control: Input },
{ label: "Min H", key: "min-height", control: Input },
{ label: "Max W", key: "max-width", control: Input },
{ label: "Max H", key: "max-height", control: Input },
]
export const position = [
{
label: "Position",
key: "position",
control: OptionSelect,
options: [
{ label: "static" },
{ label: "relative" },
{ label: "fixed" },
{ label: "absolute" },
{ label: "sticky" },
],
},
]
export const typography = [
{
label: "Font",
key: "font-family",
control: OptionSelect,
defaultValue: "initial",
options: [
"initial",
"Times New Roman",
"Georgia",
"Arial",
"Arial Black",
"Comic Sans MS",
"Impact",
"Lucida Sans Unicode",
],
styleBindingProperty: "font-family",
},
{
label: "Weight",
key: "font-weight",
control: OptionSelect,
options: [
{ label: "normal" },
{ label: "bold" },
{ label: "bolder" },
{ label: "lighter" },
],
},
{ label: "size", key: "font-size", defaultValue: "", control: Input },
{ label: "Line H", key: "line-height", control: Input },
{
label: "Color",
key: "color",
control: OptionSelect,
options: ["black", "white", "red", "blue", "green"],
},
{
label: "align",
key: "text-align",
control: OptionSelect,
options: ["initial", "left", "right", "center", "justify"],
}, //custom
{ label: "transform", key: "text-transform", control: Input }, //custom
{ label: "style", key: "font-style", control: Input }, //custom
]
export const background = [
{
label: "Background",
key: "background",
control: OptionSelect,
options: ["black", "white", "red", "blue", "green"],
},
{ label: "Image", key: "image", control: Input }, //custom
]
export const border = [
{ label: "Radius", key: "border-radius", control: Input },
{ label: "Width", key: "border-width", control: Input }, //custom
{
label: "Color",
key: "border-color",
control: OptionSelect,
options: ["black", "white", "red", "blue", "green"],
},
{ label: "Style", key: "border-style", control: Input },
]
export const effects = [
{ label: "Opacity", key: "opacity", control: Input },
{ label: "Rotate", key: "transform", control: Input }, //needs special control
{ label: "Shadow", key: "box-shadow", control: Input },
]
export const transitions = [
{ label: "Property", key: "transition-property", control: Input },
{ label: "Duration", key: "transition-timing-function", control: Input },
{ label: "Ease", key: "transition-ease", control: Input },
]
export const all = {
layout,
spacing,
size,
position,
typography,
background,
border,
effects,
transitions,
}
export function excludeProps(props, propsToExclude) {
const modifiedProps = {}
for (const prop in props) {
if (!propsToExclude.includes(prop)) {
modifiedProps[prop] = props[prop]
}
}
return modifiedProps
}

View File

@ -1,3 +1,9 @@
import Input from "../common/Input.svelte"
import OptionSelect from "./OptionSelect.svelte"
import Checkbox from "../common/Checkbox.svelte"
import { all } from "./propertyCategories.js"
export default {
categories: [
{
@ -20,6 +26,31 @@ export default {
icon: "ri-layout-row-fill",
commonProps: {},
children: [],
properties: {
design: { ...all },
settings: [
{
key: "type",
label: "Type",
control: OptionSelect,
options: [
{ label: "article" },
{ label: "aside" },
{ label: "details" },
{ label: "div" },
{ label: "figure" },
{ label: "figcaption" },
{ label: "footer" },
{ label: "header" },
{ label: "main" },
{ label: "mark" },
{ label: "nav" },
{ label: "paragraph" },
{ label: "summary" },
],
},
],
},
},
{
name: "Text",
@ -32,13 +63,21 @@ export default {
name: "Headline",
description: "A component for displaying heading text",
icon: "ri-heading",
props: {
type: {
type: "options",
options: ["h1", "h2", "h3", "h4", "h5", "h6"],
default: "h1",
properties: {
design: { ...all },
settings: [
{
key: "text",
label: "Text",
control: Input,
},
text: "string",
{
key: "type",
label: "Type",
control: OptionSelect,
options: ["h1", "h2", "h3", "h4", "h5", "h6"],
},
],
},
},
{
@ -46,7 +85,34 @@ export default {
name: "Paragraph",
description: "A component for displaying paragraph text.",
icon: "ri-paragraph",
props: {},
properties: {
design: { ...all },
settings: [
{
label: "Text",
key: "text",
control: Input,
},
{
label: "Type",
key: "type",
control: OptionSelect,
options: [
"none",
"bold",
"strong",
"italic",
"emphasis",
"mark",
"small",
"del",
"ins",
"sub",
"sup",
],
},
],
},
},
],
},
@ -62,21 +128,38 @@ export default {
description:
"A textfield component that allows the user to input text.",
icon: "ri-edit-box-line",
props: {},
properties: {
design: { ...all },
settings: [
{ label: "Label", key: "label", control: Input },
{
label: "Type",
key: "type",
control: OptionSelect,
options: ["text", "password"],
},
],
},
},
{
_component: "@budibase/standard-components/checkbox",
name: "Checkbox",
description: "A selectable checkbox component",
icon: "ri-checkbox-line",
props: {},
properties: {
design: { ...all },
settings: [{ label: "Label", key: "label", control: Input }],
},
},
{
_component: "@budibase/standard-components/radiobutton",
name: "Radiobutton",
description: "A selectable radiobutton component",
icon: "ri-radio-button-line",
props: {},
properties: {
design: { ...all },
settings: [{ label: "Label", key: "label", control: Input }],
},
},
{
_component: "@budibase/standard-components/select",
@ -84,7 +167,10 @@ export default {
description:
"A select component for choosing from different options",
icon: "ri-file-list-line",
props: {},
properties: {
design: { ...all },
settings: [],
},
},
],
},
@ -93,24 +179,51 @@ export default {
name: "Button",
description: "A basic html button that is ready for styling",
icon: "ri-radio-button-fill",
commonProps: {},
children: [],
properties: {
design: {
...all,
},
settings: [
{ label: "Text", key: "text", control: Input },
{
label: "Disabled",
key: "disabled",
valueKey: "checked",
control: Checkbox,
},
],
},
},
{
_component: "@budibase/standard-components/icon",
name: "Icon",
description: "A basic component for displaying icons",
icon: "ri-sun-fill",
commonProps: {},
children: [],
properties: {
design: { ...all },
},
},
{
_component: "@budibase/standard-components/link",
name: "Link",
description: "A basic link component for internal and external links",
icon: "ri-link",
commonProps: {},
children: [],
properties: {
design: { ...all },
settings: [
{ label: "Text", key: "text", control: Input },
{ label: "Url", key: "url", control: Input },
{
label: "Open New Tab",
key: "openInNewTab",
valueKey: "checked",
control: Checkbox,
},
],
},
},
],
},
@ -124,17 +237,16 @@ export default {
description:
"A basic card component that can contain content and actions.",
icon: "ri-layout-bottom-line",
commonProps: {},
children: [],
properties: { design: { ...all } },
},
{
_component: "@budibase/standard-components/login",
name: "Login",
description:
"A component that automatically generates a login screen for your app.",
icon: "ri-login-box-fill",
commonProps: {},
children: [],
properties: { design: { ...all } },
},
{
name: "Navigation Bar",
@ -142,8 +254,8 @@ export default {
description:
"A component for handling the navigation within your app.",
icon: "ri-navigation-fill",
commonProps: {},
children: [],
properties: { design: { ...all } },
},
],
},
@ -153,19 +265,17 @@ export default {
children: [
{
name: "Table",
_component: "@budibase/materialdesign-components/Datatable",
description: "A component that generates a table from your data.",
icon: "ri-archive-drawer-fill",
commonProps: {},
properties: { design: { ...all } },
children: [],
},
{
_component: "@budibase/materialdesign-components/Form",
name: "Form",
description: "A component that generates a form from your data.",
icon: "ri-file-edit-fill",
commonProps: {},
component: "@budibase/materialdesign-components/Form",
properties: { design: { ...all } },
_component: "@budibase/materialdesign-components/Form",
template: {
component: "@budibase/materialdesign-components/Form",
description: "Form for saving a record",
@ -178,7 +288,7 @@ export default {
name: "DataTable",
description: "A table for displaying data from the backend.",
icon: "ri-archive-drawer-fill",
commonProps: {},
properties: { design: { ...all } },
children: [],
},
{
@ -186,7 +296,7 @@ export default {
name: "DataForm",
description: "Form stuff",
icon: "ri-file-edit-fill",
commonProps: {},
properties: { design: { ...all } },
children: [],
},
{
@ -194,7 +304,7 @@ export default {
_component: "@budibase/standard-components/datachart",
description: "Shiny chart",
icon: "ri-bar-chart-line",
commonProps: {},
properties: { design: { ...all } },
children: [],
},
{
@ -202,7 +312,7 @@ export default {
_component: "@budibase/standard-components/datalist",
description: "Shiny list",
icon: "ri-file-list-line",
commonProps: {},
properties: { design: { ...all } },
children: [],
},
{
@ -210,7 +320,7 @@ export default {
_component: "@budibase/standard-components/datamap",
description: "Shiny map",
icon: "ri-map-pin-line",
commonProps: {},
properties: { design: { ...all } },
children: [],
},
],

View File

@ -38,12 +38,12 @@
height: 100%;
}
@media only screen and (min-width: 1800px) {
@media only screen and (min-width: 1800px) {
.nav {
overflow: auto;
flex: 0 1 auto;
width: 300px;
height: 100%;
}
}
}
</style>

View File

@ -1,320 +1,55 @@
import {
generate_css,
make_margin,
generate_screen_css,
generate_array_styles
} from "../src/builderStore/generate_css.js"
describe("make_margin", () => {
test("it should generate a valid rule", () => {
expect(make_margin(["1", "1", "1", "1"])).toEqual("1px 1px 1px 1px")
})
test("empty values should output 0", () => {
expect(make_margin(["1", "1", "", ""])).toEqual("1px 1px 0px 0px")
expect(make_margin(["1", "", "", "1"])).toEqual("1px 0px 0px 1px")
expect(make_margin(["", "", "", ""])).toEqual("0px 0px 0px 0px")
})
})
describe("generate_css", () => {
test("it should generate a valid css rule: grid-area", () => {
expect(generate_css({ layout: { gridarea: ["", "", "", ""] } })).toEqual({
layout: "",
position: "",
})
test("Check how partially empty arrays are handled", () => {
expect(["", "5", "", ""].map(generate_array_styles)).toEqual(["0px", "5px", "0px", "0px"])
})
test("it should generate a valid css rule: grid-gap", () => {
expect(generate_css({ layout: { gap: "10" } })).toEqual({
layout: "grid-gap: 10px;\ndisplay: grid;",
position: "",
})
test("Check how array styles are output", () => {
expect(generate_css({ margin: ["0", "10", "0", "15"] })).toBe("margin: 0px 10px 0px 15px;")
})
test("it should generate a valid css rule: column 1", () => {
expect(generate_css({ position: { column: ["", ""] } })).toEqual({
layout: "",
position: "",
})
test("Check handling of an array with empty string values", () => {
expect(generate_css({ padding: ["", "", "", ""] })).toBe("")
})
test("it should generate a valid css rule: column 2", () => {
expect(generate_css({ position: { column: ["1", ""] } })).toEqual({
position: "grid-column-start: 1;",
layout: "",
})
test("Check handling of an empty array", () => {
expect(generate_css({ margin: [] })).toBe("")
})
test("it should generate a valid css rule: column 3", () => {
expect(generate_css({ position: { column: ["", "1"] } })).toEqual({
position: "grid-column-end: 1;",
layout: "",
})
})
test("it should generate a valid css rule: column 4", () => {
expect(generate_css({ position: { column: ["1", "1"] } })).toEqual({
position: "grid-column-start: 1;\ngrid-column-end: 1;",
layout: "",
})
})
test("it should generate a valid css rule: row 1", () => {
expect(generate_css({ position: { row: ["", ""] } })).toEqual({
layout: "",
position: "",
})
})
test("it should generate a valid css rule: row 2", () => {
expect(generate_css({ position: { row: ["1", ""] } })).toEqual({
position: "grid-row-start: 1;",
layout: "",
})
})
test("it should generate a valid css rule: row 3", () => {
expect(generate_css({ position: { row: ["", "1"] } })).toEqual({
position: "grid-row-end: 1;",
layout: "",
})
})
test("it should generate a valid css rule: row 4", () => {
expect(generate_css({ position: { row: ["1", "1"] } })).toEqual({
position: "grid-row-start: 1;\ngrid-row-end: 1;",
layout: "",
})
})
test("it should generate a valid css rule: padding 1", () => {
expect(
generate_css({ position: { padding: ["1", "1", "1", "1"] } })
).toEqual({
position: "padding: 1px 1px 1px 1px;",
layout: "",
})
})
test("it should generate a valid css rule: padding 2", () => {
expect(generate_css({ position: { padding: ["1", "", "", "1"] } })).toEqual(
{
position: "padding: 1px 0px 0px 1px;",
layout: "",
}
)
})
test("it should generate a valid css rule: margin 1", () => {
expect(
generate_css({ position: { margin: ["1", "1", "1", "1"] } })
).toEqual({
position: "margin: 1px 1px 1px 1px;",
layout: "",
})
})
test("it should generate a valid css rule: margin 2", () => {
expect(generate_css({ position: { margin: ["1", "", "", "1"] } })).toEqual({
position: "margin: 1px 0px 0px 1px;",
layout: "",
})
})
test("it should generate a valid css rule: z-index 1", () => {
expect(generate_css({ position: { zindex: "" } })).toEqual({
position: "",
layout: "",
})
})
test("it should generate a valid css rule: z-index 2", () => {
expect(generate_css({ position: { zindex: "1" } })).toEqual({
position: "z-index: 1;",
layout: "",
})
test("Check handling of valid font property", () => {
expect(generate_css({ "font-size": "10px" })).toBe("font-size: 10px;")
})
})
describe("generate_screen_css", () => {
test("it should compile the css for a list of components", () => {
const components = [
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] },
},
_id: 1,
},
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] },
},
_id: 2,
},
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] },
},
_id: 3,
},
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] },
},
_id: 4,
},
]
const normalComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: { "font-size": "16px" }, hover: {}, active: {}, selected: {} } }
const compiled = `.pos-1 {
margin: 1px 1px 1px 1px;
}
.lay-1 {
}
.pos-2 {
margin: 1px 1px 1px 1px;
}
.lay-2 {
}
.pos-3 {
margin: 1px 1px 1px 1px;
}
.lay-3 {
}
.pos-4 {
margin: 1px 1px 1px 1px;
}
.lay-4 {
}`
expect(generate_screen_css(components)).toEqual(compiled)
test("Test generation of normal css styles", () => {
expect(generate_screen_css([normalComponent])).toBe(".header-123-456 {\nfont-size: 16px;\n}")
})
test("it should compile the css for a list of components", () => {
const components = [
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] },
},
_id: 1,
_children: [
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] },
},
_id: 2,
_children: [
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] },
},
_id: 3,
_children: [
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] },
},
_id: 4,
_children: [
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] },
},
_id: 5,
_children: [],
},
],
},
],
},
],
},
],
},
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] },
},
_id: 6,
},
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] },
},
_id: 7,
},
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] },
},
_id: 8,
},
]
const hoverComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {"font-size": "16px"}, active: {}, selected: {} } }
const compiled = `.pos-1 {
margin: 1px 1px 1px 1px;
}
.lay-1 {
test("Test generation of hover css styles", () => {
expect(generate_screen_css([hoverComponent])).toBe(".header-123-456:hover {\nfont-size: 16px;\n}")
})
}
.pos-2 {
margin: 1px 1px 1px 1px;
}
.lay-2 {
const selectedComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: { "font-size": "16px" } } }
}
.pos-3 {
margin: 1px 1px 1px 1px;
}
.lay-3 {
test("Test generation of selection css styles", () => {
expect(generate_screen_css([selectedComponent])).toBe(".header-123-456::selection {\nfont-size: 16px;\n}")
})
}
.pos-4 {
margin: 1px 1px 1px 1px;
}
.lay-4 {
const emptyComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: {} } }
}
.pos-5 {
margin: 1px 1px 1px 1px;
}
.lay-5 {
}
.pos-6 {
margin: 1px 1px 1px 1px;
}
.lay-6 {
}
.pos-7 {
margin: 1px 1px 1px 1px;
}
.lay-7 {
}
.pos-8 {
margin: 1px 1px 1px 1px;
}
.lay-8 {
}`
expect(generate_screen_css(components)).toEqual(compiled)
test.only("Testing handling of empty component styles", () => {
expect(generate_screen_css([emptyComponent])).toBe("")
})
})

View File

@ -9,8 +9,10 @@
"_id": 0,
"type": "div",
"_styles": {
"layout": {},
"position": {}
"normal": {},
"hover": {},
"active": {},
"selected": {}
},
"_code": ""
},

View File

@ -9,8 +9,10 @@
"_id": 1,
"type": "div",
"_styles": {
"layout": {},
"position": {}
"normal": {},
"hover": {},
"active": {},
"selected": {}
},
"_code": ""
},

View File

@ -9,8 +9,10 @@
"_id": 0,
"type": "div",
"_styles": {
"layout": {},
"position": {}
"normal": {},
"hover": {},
"active": {},
"selected": {}
},
"_code": ""
},

View File

@ -31,7 +31,7 @@ export const attachChildren = initialiseOpts => (htmlElement, options) => {
}
}
htmlElement.classList.add(`lay-${treeNode.props._id}`)
// htmlElement.classList.add(`lay-${treeNode.props._id}`)
const childNodes = []
for (let childProps of treeNode.props._children) {

View File

@ -35,8 +35,9 @@ export const prepareRenderComponent = ({
thisNode.rootElement =
htmlElement.children[htmlElement.children.length - 1]
let [componentName] = props._component.match(/[a-z]*$/)
if (props._id && thisNode.rootElement) {
thisNode.rootElement.classList.add(`pos-${props._id}`)
thisNode.rootElement.classList.add(`${componentName}-${props._id}`)
}
}
}

View File

@ -12,10 +12,10 @@
"props": {
"logoUrl": "string",
"title": "string",
"backgroundColor": "colour",
"color": "colour",
"backgroundColor": "string",
"color": "string",
"borderWidth": "string",
"borderColor": "colour",
"borderColor": "string",
"borderStyle": "string"
}
},
@ -23,35 +23,10 @@
"name": "Button",
"description": "an html <button />",
"props": {
"contentText": {
"type": "string",
"default": "Button"
},
"text": "string",
"className": "string",
"disabled": "bool",
"onClick": "event",
"background": "colour",
"color": "colour",
"border": "string",
"padding": "string",
"hoverColor": "string",
"hoverBackground": "string",
"hoverBorder": "string",
"fontFamily": {
"type": "options",
"default": "initial",
"styleBindingProperty": "font-family",
"options": [
"initial",
"Times New Roman",
"Georgia",
"Arial",
"Arial Black",
"Comic Sans MS",
"Impact",
"Lucida Sans Unicode"
]
}
"onClick": "event"
},
"tags": [
"layout"
@ -167,58 +142,7 @@
"children": false,
"props": {
"text": "string",
"color": "colour",
"fontFamily": {
"type": "options",
"default": "initial",
"styleBindingProperty": "font-family",
"options": [
"initial",
"Times New Roman",
"Georgia",
"Arial",
"Arial Black",
"Comic Sans MS",
"Impact",
"Lucida Sans Unicode"
]
},
"fontSize": "string",
"textAlign": {
"type": "options",
"default": "inline",
"options": [
"left",
"center",
"right"
]
},
"verticalAlign": {
"type": "options",
"default": "inline",
"options": [
"top",
"middle",
"bottom"
]
},
"formattingTag": {
"type": "options",
"default": "none",
"options": [
"none",
"<b> - bold",
"<strong> - important",
"<i> - italic",
"<em> - emphasized",
"<mark> - marked text",
"<small> - small",
"<del> - deleted",
"<ins> - inserted",
"<sub> - subscript",
"<sup> - superscript"
]
}
"type": {"type": "string", "default": "none"}
},
"tags": [
"div",
@ -230,6 +154,7 @@
"description": "A component that allows the user to input text.",
"props": {
"label": "string",
"type": "string",
"value": "string",
"onchange": "event"
}
@ -259,7 +184,7 @@
"props": {
"icon": "string",
"fontSize": "string",
"color": "colour"
"color": "string"
}
},
"datatable": {
@ -328,8 +253,8 @@
"url": "string",
"openInNewTab": "bool",
"text": "string",
"color": "colour",
"hoverColor": "colour",
"color": "string",
"hoverColor": "string",
"underline": "bool",
"fontSize": "string",
"fontFamily": {
@ -383,25 +308,6 @@
"summary"
],
"default": "div"
},
"backgroundColor": "string",
"color": "string",
"borderWidth": "string",
"borderColor": "string",
"borderStyle": {
"type": "options",
"options": [
"none",
"solid",
"dotted",
"dashed",
"double",
"groove",
"ridge",
"inset",
"outset"
],
"default": "none"
}
},
"container": true,
@ -416,7 +322,6 @@
"description": "An HTML H1 - H6 tag",
"props": {
"className": "string",
"color":"colour",
"text": "string",
"type": {
"type": "options",
@ -429,21 +334,6 @@
"h5",
"h6"
]
},
"fontFamily": {
"type": "options",
"default": "initial",
"styleBindingProperty": "font-family",
"options": [
"initial",
"Times New Roman",
"Georgia",
"Arial",
"Arial Black",
"Comic Sans MS",
"Impact",
"Lucida Sans Unicode"
]
}
},
"tags": []

View File

@ -1,58 +1,15 @@
<script>
import { cssVars, createClasses } from "./cssVars"
import { buildStyle } from "./buildStyle"
export let className = "default"
export let disabled = false
export let contentText
export let text
export let onClick
export let background
export let color
export let border
export let padding
export let hoverColor
export let hoverBackground
export let hoverBorder
export let fontFamily
export let _bb
let theButton
let cssVariables
let buttonStyles
let customHoverColorClass
let customHoverBorderClass
let customHoverBackClass
let customClasses = ""
$: if (_bb.props._children && _bb.props._children.length > 0)
theButton && _bb.attachChildren(theButton)
$: {
cssVariables = {
hoverColor,
hoverBorder,
hoverBackground,
background,
color,
border,
}
buttonStyles = buildStyle({
padding,
"font-family": fontFamily,
})
customClasses = createClasses({
hoverColor,
hoverBorder,
hoverBackground,
background,
border,
color,
})
}
const clickHandler = () => {
_bb.call(onClick)
}
@ -60,15 +17,10 @@
<button
bind:this={theButton}
use:cssVars={cssVariables}
class="{className}
{customClasses}"
class={className}
disabled={disabled || false}
on:click={clickHandler}
style={buttonStyles}>
{#if !_bb.props._children || _bb.props._children.length === 0}
{contentText}
{/if}
on:click={clickHandler}>
{#if !_bb.props._children || _bb.props._children.length === 0}{text}{/if}
</button>
<style>

View File

@ -4,26 +4,12 @@
export let className = ""
export let onLoad
export let type = "div"
export let backgroundColor
export let color
export let borderWidth
export let borderColor
export let borderStyle
export let _bb
let containerElement
let hasLoaded
let currentChildren
$: cssVariables = {
backgroundColor,
color,
borderWidth,
borderColor,
borderStyle,
}
$: classes = `${createClasses(cssVariables)} ${className}`
$: {
if (containerElement) {
_bb.attachChildren(containerElement)
@ -36,87 +22,29 @@
</script>
{#if type === 'div'}
<div
class={classes}
bind:this={containerElement}
use:cssVars={cssVariables} />
<div bind:this={containerElement} />
{:else if type === 'header'}
<header
class={classes}
bind:this={containerElement}
use:cssVars={cssVariables} />
<header bind:this={containerElement} />
{:else if type === 'main'}
<main
class={classes}
bind:this={containerElement}
use:cssVars={cssVariables} />
<main bind:this={containerElement} />
{:else if type === 'footer'}
<footer
class={classes}
bind:this={containerElement}
use:cssVars={cssVariables} />
<footer bind:this={containerElement} />
{:else if type === 'aside'}
<aside
class={classes}
bind:this={containerElement}
use:cssVars={cssVariables} />
<aside bind:this={containerElement} />
{:else if type === 'summary'}
<summary
class={classes}
bind:this={containerElement}
use:cssVars={cssVariables} />
<summary bind:this={containerElement} />
{:else if type === 'details'}
<details
class={classes}
bind:this={containerElement}
use:cssVars={cssVariables} />
<details bind:this={containerElement} />
{:else if type === 'article'}
<article
class={classes}
bind:this={containerElement}
use:cssVars={cssVariables} />
<article bind:this={containerElement} />
{:else if type === 'nav'}
<nav
class={classes}
bind:this={containerElement}
use:cssVars={cssVariables} />
<nav bind:this={containerElement} />
{:else if type === 'mark'}
<mark
class={classes}
bind:this={containerElement}
use:cssVars={cssVariables} />
<mark bind:this={containerElement} />
{:else if type === 'figure'}
<figure
class={classes}
bind:this={containerElement}
use:cssVars={cssVariables} />
<figure bind:this={containerElement} />
{:else if type === 'figcaption'}
<figcaption
class={classes}
bind:this={containerElement}
use:cssVars={cssVariables} />
<figcaption bind:this={containerElement} />
{:else if type === 'paragraph'}
<p class={classes} bind:this={containerElement} use:cssVars={cssVariables} />
<p bind:this={containerElement} />
{/if}
<style>
.backgroundColor {
background-color: var(--backgroundColor);
}
.color {
color: var(--color);
}
.borderColor {
border-color: var(--borderColor);
}
.borderWidth {
border-width: var(--borderWidth);
}
.borderStyle {
border-style: var(--borderStyle);
}
</style>

View File

@ -2,28 +2,25 @@
import { buildStyle } from "./buildStyle.js"
export let className = ""
export let type
export let _bb
export let text = ""
export let fontFamily = ""
export let color = ""
export let _bb
let containerElement
$: containerElement && !text && _bb.attachChildren(containerElement)
$: style = buildStyle({ "font-family": fontFamily, color })
// $: console.log("HEADING", color)
</script>
{#if type === 'h1'}
<h1 class={className} {style} bind:this={containerElement}>{text}</h1>
<h1 class={className} bind:this={containerElement}>{text}</h1>
{:else if type === 'h2'}
<h2 class={className} {style} bind:this={containerElement}>{text}</h2>
<h2 class={className} bind:this={containerElement}>{text}</h2>
{:else if type === 'h3'}
<h3 class={className} {style} bind:this={containerElement}>{text}</h3>
<h3 class={className} bind:this={containerElement}>{text}</h3>
{:else if type === 'h4'}
<h4 class={className} {style} bind:this={containerElement}>{text}</h4>
<h4 class={className} bind:this={containerElement}>{text}</h4>
{:else if type === 'h5'}
<h5 class={className} {style} bind:this={containerElement}>{text}</h5>
<h5 class={className} bind:this={containerElement}>{text}</h5>
{:else if type === 'h6'}
<h6 class={className} {style} bind:this={containerElement}>{text}</h6>
<h6 class={className} bind:this={containerElement}>{text}</h6>
{/if}

View File

@ -4,11 +4,6 @@
export let url = ""
export let text = ""
export let openInNewTab = false
export let color
export let hoverColor
export let underline = false
export let fontFamily
export let fontSize
export let _bb
@ -16,43 +11,12 @@
$: anchorElement && !text && _bb.attachChildren(anchorElement)
$: target = openInNewTab ? "_blank" : "_self"
$: cssVariables = {
hoverColor,
color,
textDecoration: underline ? "underline" : "none",
fontSize,
fontFamily,
}
$: classes = createClasses(cssVariables)
</script>
<a
href={url}
bind:this={anchorElement}
class={classes}
{target}
use:cssVars={cssVariables}>
{text}
</a>
<a href={url} bind:this={anchorElement} {target}>{text}</a>
<style>
.color {
color: var(--color);
}
.hoverColor:hover {
color: var(--color);
}
.textDecoration {
text-decoration: var(--textDecoration);
}
.fontSize {
font-size: var(--fontSize);
}
.fontFamily {
font-family: var(--fontFamily);
}
</style>

View File

@ -4,45 +4,35 @@
export let text = ""
export let className = ""
export let formattingTag = ""
export let fontFamily = ""
export let fontSize = "1em"
export let textAlign = ""
export let verticalAlign = ""
export let color = ""
export let type = ""
export let _bb
const isTag = tag => (formattingTag || "").indexOf(tag) > -1
$: style = buildStyle({
"font-size": fontSize,
"font-family": fontFamily,
color,
})
const isTag = tag => type === tag
</script>
{#if isTag('none')}
<span {style}>{text}</span>
{:else if isTag('<b>')}
<b class={className} {style}>{text}</b>
{:else if isTag('<strong>')}
<strong class={className} {style}>{text}</strong>
{:else if isTag('<i>')}
<i class={className} {style}>{text}</i>
{:else if isTag('<em>')}
<em class={className} {style}>{text}</em>
{:else if isTag('<mark>')}
<mark class={className} {style}>{text}</mark>
{:else if isTag('<small>')}
<small class={className} {style}>{text}</small>
{:else if isTag('<del>')}
<del class={className} {style}>{text}</del>
{:else if isTag('<ins>')}
<ins class={className} {style}>{text}</ins>
{:else if isTag('<sub>')}
<sub class={className} {style}>{text}</sub>
{:else if isTag('<sup>')}
<sup class={className} {style}>{text}</sup>
{:else}{text}{/if}
<span>{text}</span>
{:else if isTag('bold')}
<b class={className}>{text}</b>
{:else if isTag('strong')}
<strong class={className}>{text}</strong>
{:else if isTag('italic')}
<i class={className}>{text}</i>
{:else if isTag('emphasis')}
<em class={className}>{text}</em>
{:else if isTag('mark')}
<mark class={className}>{text}</mark>
{:else if isTag('small')}
<small class={className}>{text}</small>
{:else if isTag('del')}
<del class={className}>{text}</del>
{:else if isTag('ins')}
<ins class={className}>{text}</ins>
{:else if isTag('sub')}
<sub class={className}>{text}</sub>
{:else if isTag('sup')}
<sup class={className}>{text}</sup>
{:else}
<span>{text}</span>
{/if}