forked from tsai/budibase
19 changed files with 950 additions and 248 deletions
@ -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> |
|||
@ -0,0 +1,62 @@ |
|||
<script> |
|||
import PropertyGroup from "./PropertyGroup.svelte" |
|||
import FlatButtonGroup from "./FlatButtonGroup.svelte" |
|||
|
|||
export let panelDefinition = {} |
|||
export let componentInstance = {} |
|||
export let componentDefinition = {} |
|||
export let onPropChanged = () => {} |
|||
|
|||
let selectedCategory = "desktop" |
|||
|
|||
const getProperties = name => panelDefinition.properties[name] |
|||
|
|||
function onChange(category) { |
|||
selectedCategory = category |
|||
} |
|||
|
|||
const buttonProps = [ |
|||
{ value: "desktop", text: "Desktop" }, |
|||
{ value: "mobile", text: "Mobile" }, |
|||
{ value: "hover", text: "Hover" }, |
|||
{ value: "active", text: "Active" }, |
|||
{ value: "selected", text: "Selected" }, |
|||
] |
|||
|
|||
$: propertyGroupNames = |
|||
!!panelDefinition.properties && Object.keys(panelDefinition.properties) |
|||
</script> |
|||
|
|||
<div class="design-view-container"> |
|||
|
|||
<div class="design-view-state-categories"> |
|||
<FlatButtonGroup value={selectedCategory} {buttonProps} {onChange} /> |
|||
</div> |
|||
|
|||
<div class="design-view-property-groups"> |
|||
{#each propertyGroupNames as groupName} |
|||
<PropertyGroup |
|||
name={groupName} |
|||
properties={getProperties(groupName)} |
|||
{onPropChanged} |
|||
{componentDefinition} |
|||
{componentInstance} /> |
|||
{/each} |
|||
</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; |
|||
} |
|||
</style> |
|||
@ -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> |
|||
@ -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> |
|||
@ -0,0 +1,33 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
export let value = "" |
|||
export let onChange = value => {} |
|||
export let options = [] |
|||
export let initialValue = "" |
|||
export let styleBindingProperty = "" |
|||
|
|||
$: bindOptionToStyle = !!styleBindingProperty |
|||
|
|||
onMount(() => { |
|||
if (!value && !!initialValue) { |
|||
value = initialValue |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<select |
|||
class="uk-select uk-form-small" |
|||
{value} |
|||
on:change={ev => onChange(ev.target.value)}> |
|||
{#each options as { value, label }} |
|||
{#if bindOptionToStyle} |
|||
<option |
|||
style={`${styleBindingProperty}: ${value || label};`} |
|||
value={value || label}> |
|||
{label} |
|||
</option> |
|||
{:else} |
|||
<option value={value || label}>{label}</option> |
|||
{/if} |
|||
{/each} |
|||
</select> |
|||
@ -0,0 +1,41 @@ |
|||
<script> |
|||
export let label = "" |
|||
export let control = null |
|||
export let value = "" |
|||
export let props = {} |
|||
export let onChange = () => {} |
|||
</script> |
|||
|
|||
<div class="property-control"> |
|||
<div class="label">{label}</div> |
|||
<div class="control"> |
|||
<svelte:component |
|||
this={control} |
|||
{value} |
|||
on:change={onChange} |
|||
{onChange} |
|||
{...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> |
|||
@ -0,0 +1,101 @@ |
|||
<script> |
|||
import { excludeProps } from "./propertyCategories.js" |
|||
import PropertyControl from "./PropertyControl.svelte" |
|||
|
|||
export let name = "" |
|||
export let properties = {} |
|||
export let componentInstance = {} |
|||
export let componentDefinition = {} |
|||
export let onPropChanged = () => {} |
|||
|
|||
export let show = false |
|||
let showComponentGroup = false |
|||
|
|||
const propExistsOnComponentDef = prop => prop in componentDefinition.props |
|||
const capitalize = name => name[0].toUpperCase() + name.slice(1) |
|||
|
|||
function onChange(key, v) { |
|||
!!v.target ? onPropChanged(key, v.target.value) : onPropChanged(key, v) |
|||
} |
|||
|
|||
$: { |
|||
let res = false |
|||
let componentProps = Object.keys(componentDefinition.props) |
|||
for (let prop in properties) { |
|||
if (componentProps.includes(prop)) { |
|||
showComponentGroup = true |
|||
} |
|||
if (showComponentGroup) break |
|||
} |
|||
} |
|||
|
|||
$: propertyDefinition = Object.entries(properties) |
|||
$: icon = show ? "ri-arrow-down-s-fill" : "ri-arrow-right-s-fill" |
|||
</script> |
|||
|
|||
<!-- {#if showComponentGroup} --> |
|||
<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 propertyDefinition as [key, definition]} |
|||
<!-- {#if propExistsOnComponentDef(key)} --> |
|||
<PropertyControl |
|||
label={definition.label || capitalize(key)} |
|||
control={definition.control} |
|||
value={componentInstance[key]} |
|||
onChange={value => onChange(key, value)} |
|||
props={{ ...excludeProps(definition, ['control']) }} /> |
|||
<!-- {/if} --> |
|||
{/each} |
|||
</div> |
|||
</div> |
|||
<!-- {/if} --> |
|||
|
|||
<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> |
|||
@ -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> |
|||
@ -0,0 +1,177 @@ |
|||
import Input from "../common/Input.svelte" |
|||
import OptionSelect from "./OptionSelect.svelte" |
|||
/* |
|||
TODO: Allow for default values for all properties |
|||
*/ |
|||
|
|||
export const general = { |
|||
text: { control: Input }, |
|||
} |
|||
|
|||
export const layout = { |
|||
flexDirection: { |
|||
label: "Direction", |
|||
control: OptionSelect, |
|||
initialValue: "columnReverse", |
|||
options: [ |
|||
{ label: "row" }, |
|||
{ label: "row-reverse", value: "rowReverse" }, |
|||
{ label: "column" }, |
|||
{ label: "column-reverse", value: "columnReverse" }, |
|||
], |
|||
}, |
|||
justifyContent: { label: "Justify", control: Input }, |
|||
alignItems: { label: "Align", control: Input }, |
|||
flexWrap: { |
|||
label: "Wrap", |
|||
control: OptionSelect, |
|||
options: [{ label: "wrap" }, { label: "no wrap", value: "noWrap" }], |
|||
}, |
|||
} |
|||
|
|||
export const spacing = { |
|||
padding: { control: Input }, |
|||
margin: { control: Input }, |
|||
} |
|||
|
|||
export const size = { |
|||
width: { control: Input }, |
|||
height: { control: Input }, |
|||
minWidth: { label: "Min W", control: Input }, |
|||
minHeight: { label: "Min H", control: Input }, |
|||
maxWidth: { label: "Max W", control: Input }, |
|||
maxHeight: { label: "Max H", control: Input }, |
|||
overflow: { |
|||
control: OptionSelect, |
|||
options: [ |
|||
{ label: "visible" }, |
|||
{ label: "auto" }, |
|||
{ label: "hidden" }, |
|||
{ label: "auto" }, |
|||
{ label: "scroll" }, |
|||
], |
|||
}, //custom
|
|||
} |
|||
|
|||
export const position = { |
|||
position: { |
|||
control: OptionSelect, |
|||
options: [ |
|||
{ label: "static" }, |
|||
{ label: "relative" }, |
|||
{ label: "fixed" }, |
|||
{ label: "absolute" }, |
|||
{ label: "sticky" }, |
|||
], |
|||
}, |
|||
} |
|||
|
|||
export const typography = { |
|||
fontFamily: { |
|||
label: "Font", |
|||
control: OptionSelect, |
|||
options: [ |
|||
{ label: "initial" }, |
|||
{ label: "Times New Roman" }, |
|||
{ label: "Georgia" }, |
|||
{ label: "Arial" }, |
|||
{ label: "Arial Black" }, |
|||
{ label: "Comic Sans MS" }, |
|||
{ label: "Impact" }, |
|||
{ label: "Lucida Sans Unicode" }, |
|||
], |
|||
styleBindingProperty: "font-family", |
|||
}, |
|||
fontWeight: { |
|||
label: "weight", |
|||
control: OptionSelect, |
|||
options: [ |
|||
{ label: "normal" }, |
|||
{ label: "bold" }, |
|||
{ label: "bolder" }, |
|||
{ label: "lighter" }, |
|||
], |
|||
}, |
|||
fontSize: { label: "size", control: Input }, |
|||
lineHeight: { label: "Line H", control: Input }, |
|||
color: { |
|||
control: OptionSelect, |
|||
options: [ |
|||
{ label: "black" }, |
|||
{ label: "red" }, |
|||
{ label: "white" }, |
|||
{ label: "blue" }, |
|||
{ label: "green" }, |
|||
], |
|||
}, |
|||
textAlign: { |
|||
label: "align", |
|||
control: OptionSelect, |
|||
options: [ |
|||
{ label: "initial" }, |
|||
{ label: "left" }, |
|||
{ label: "right" }, |
|||
{ label: "center" }, |
|||
{ label: "justify" }, |
|||
], |
|||
}, //custom
|
|||
textTransform: { label: "transform", control: Input }, //custom
|
|||
fontStyle: { label: "style", control: Input }, //custom
|
|||
} |
|||
|
|||
export const background = { |
|||
backgroundColor: { |
|||
label: "Background Color", |
|||
control: OptionSelect, |
|||
options: [ |
|||
{ label: "white" }, |
|||
{ label: "red" }, |
|||
{ label: "blue" }, |
|||
{ label: "green" }, |
|||
{ label: "black" }, |
|||
], |
|||
}, |
|||
image: { control: Input }, //custom
|
|||
} |
|||
|
|||
export const border = { |
|||
borderRadius: { label: "radius", control: Input }, |
|||
borderWidth: { label: "width", control: Input }, //custom
|
|||
borderColor: { label: "color", control: Input }, |
|||
borderStyle: { label: "style", control: Input }, |
|||
} |
|||
|
|||
export const effects = { |
|||
opacity: { control: Input }, |
|||
rotate: { control: Input }, |
|||
shadow: { control: Input }, |
|||
} |
|||
|
|||
export const transitions = { |
|||
property: { control: Input }, |
|||
duration: { control: Input }, |
|||
ease: { control: Input }, |
|||
} |
|||
|
|||
export const all = { |
|||
general, |
|||
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 |
|||
} |
|||
Loading…
Reference in new issue