mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
64 changed files with 1886 additions and 1420 deletions
@ -0,0 +1,239 @@ |
|||
<script> |
|||
import { createEventDispatcher } from "svelte" |
|||
import "@spectrum-css/popover/dist/index-vars.css" |
|||
import clickOutside from "../Actions/click_outside" |
|||
import { fly } from "svelte/transition" |
|||
import Icon from "../Icon/Icon.svelte" |
|||
import Input from "../Form/Input.svelte" |
|||
import { capitalise } from "../utils/helpers" |
|||
|
|||
export let value |
|||
export let size = "M" |
|||
|
|||
let open = false |
|||
|
|||
$: color = value || "transparent" |
|||
$: customValue = getCustomValue(value) |
|||
$: checkColor = getCheckColor(value) |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
const categories = [ |
|||
{ |
|||
label: "Grays", |
|||
colors: [ |
|||
"white", |
|||
"gray-100", |
|||
"gray-200", |
|||
"gray-300", |
|||
"gray-400", |
|||
"gray-500", |
|||
"gray-600", |
|||
"gray-700", |
|||
"gray-800", |
|||
"gray-900", |
|||
"black", |
|||
], |
|||
}, |
|||
{ |
|||
label: "Colors", |
|||
colors: [ |
|||
"red-400", |
|||
"orange-400", |
|||
"yellow-400", |
|||
"green-400", |
|||
"seafoam-400", |
|||
"blue-400", |
|||
"indigo-400", |
|||
"magenta-400", |
|||
|
|||
"red-500", |
|||
"orange-500", |
|||
"yellow-500", |
|||
"green-500", |
|||
"seafoam-500", |
|||
"blue-500", |
|||
"indigo-500", |
|||
"magenta-500", |
|||
|
|||
"red-600", |
|||
"orange-600", |
|||
"yellow-600", |
|||
"green-600", |
|||
"seafoam-600", |
|||
"blue-600", |
|||
"indigo-600", |
|||
"magenta-600", |
|||
|
|||
"red-700", |
|||
"orange-700", |
|||
"yellow-700", |
|||
"green-700", |
|||
"seafoam-700", |
|||
"blue-700", |
|||
"indigo-700", |
|||
"magenta-700", |
|||
], |
|||
}, |
|||
] |
|||
|
|||
const onChange = value => { |
|||
dispatch("change", value) |
|||
open = false |
|||
} |
|||
|
|||
const getCustomValue = value => { |
|||
if (!value) { |
|||
return value |
|||
} |
|||
let found = false |
|||
const comparisonValue = value.substring(35, value.length - 1) |
|||
for (let category of categories) { |
|||
found = category.colors.includes(comparisonValue) |
|||
if (found) { |
|||
break |
|||
} |
|||
} |
|||
return found ? null : value |
|||
} |
|||
|
|||
const prettyPrint = color => { |
|||
return capitalise(color.split("-").join(" ")) |
|||
} |
|||
|
|||
const getCheckColor = value => { |
|||
return /^.*(white|(gray-(50|75|100|200|300|400|500)))\)$/.test(value) |
|||
? "black" |
|||
: "white" |
|||
} |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<div |
|||
class="preview size--{size || 'M'}" |
|||
style="background: {color};" |
|||
on:click={() => (open = true)} |
|||
/> |
|||
{#if open} |
|||
<div |
|||
use:clickOutside={() => (open = false)} |
|||
transition:fly={{ y: -20, duration: 200 }} |
|||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" |
|||
> |
|||
{#each categories as category} |
|||
<div class="category"> |
|||
<div class="heading">{category.label}</div> |
|||
<div class="colors"> |
|||
{#each category.colors as color} |
|||
<div |
|||
on:click={() => { |
|||
onChange(`var(--spectrum-global-color-static-${color})`) |
|||
}} |
|||
class="color" |
|||
style="background: var(--spectrum-global-color-static-{color}); color: {checkColor};" |
|||
title={prettyPrint(color)} |
|||
> |
|||
{#if value === `var(--spectrum-global-color-static-${color})`} |
|||
<Icon name="Checkmark" size="S" /> |
|||
{/if} |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
</div> |
|||
{/each} |
|||
<div class="category category--custom"> |
|||
<div class="heading">Custom</div> |
|||
<div class="custom"> |
|||
<Input |
|||
updateOnChange={false} |
|||
quiet |
|||
placeholder="Hex, RGB, HSL..." |
|||
value={customValue} |
|||
on:change |
|||
/> |
|||
<Icon |
|||
size="S" |
|||
name="Close" |
|||
hoverable |
|||
on:click={() => onChange(null)} |
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
position: relative; |
|||
} |
|||
.preview { |
|||
width: 32px; |
|||
height: 32px; |
|||
border-radius: 100%; |
|||
transition: border-color 130ms ease-in-out; |
|||
box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-300); |
|||
} |
|||
.preview:hover { |
|||
cursor: pointer; |
|||
box-shadow: 0 0 2px 2px var(--spectrum-global-color-gray-300); |
|||
} |
|||
.size--S { |
|||
width: 20px; |
|||
height: 20px; |
|||
} |
|||
.size--M { |
|||
width: 32px; |
|||
height: 32px; |
|||
} |
|||
.size--L { |
|||
width: 48px; |
|||
height: 48px; |
|||
} |
|||
.spectrum-Popover { |
|||
width: 210px; |
|||
z-index: 999; |
|||
top: 100%; |
|||
padding: var(--spacing-l) var(--spacing-xl); |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
gap: var(--spacing-xl); |
|||
} |
|||
.colors { |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; |
|||
gap: var(--spacing-xs); |
|||
} |
|||
.heading { |
|||
font-size: var(--font-size-s); |
|||
font-weight: 600; |
|||
letter-spacing: 0.14px; |
|||
flex: 1 1 auto; |
|||
text-transform: uppercase; |
|||
grid-column: 1 / 5; |
|||
margin-bottom: var(--spacing-s); |
|||
} |
|||
.color { |
|||
height: 16px; |
|||
width: 16px; |
|||
border-radius: 100%; |
|||
box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-300); |
|||
display: grid; |
|||
place-items: center; |
|||
} |
|||
.color:hover { |
|||
cursor: pointer; |
|||
box-shadow: 0 0 2px 2px var(--spectrum-global-color-gray-300); |
|||
} |
|||
.custom { |
|||
display: grid; |
|||
grid-template-columns: 1fr auto; |
|||
align-items: center; |
|||
gap: var(--spacing-m); |
|||
margin-right: var(--spacing-xs); |
|||
} |
|||
.category--custom .heading { |
|||
margin-bottom: var(--spacing-xs); |
|||
} |
|||
</style> |
|||
@ -1,40 +0,0 @@ |
|||
<script> |
|||
export let categories = [] |
|||
export let selectedCategory = {} |
|||
export let onClick = () => {} |
|||
</script> |
|||
|
|||
<div class="tabs"> |
|||
{#each categories as category} |
|||
<li |
|||
data-cy={category.name} |
|||
on:click={() => onClick(category)} |
|||
class:active={selectedCategory === category} |
|||
> |
|||
{category.name} |
|||
</li> |
|||
{/each} |
|||
</div> |
|||
|
|||
<style> |
|||
.tabs { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
list-style: none; |
|||
font-size: var(--font-size-m); |
|||
font-weight: 600; |
|||
height: 24px; |
|||
} |
|||
|
|||
li { |
|||
color: var(--grey-5); |
|||
cursor: pointer; |
|||
margin-right: 20px; |
|||
} |
|||
|
|||
.active { |
|||
color: var(--ink); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,60 @@ |
|||
<script> |
|||
import { |
|||
TextArea, |
|||
DetailSummary, |
|||
ActionButton, |
|||
Drawer, |
|||
DrawerContent, |
|||
Layout, |
|||
Body, |
|||
Button, |
|||
} from "@budibase/bbui" |
|||
import { store } from "builderStore" |
|||
|
|||
export let componentInstance |
|||
|
|||
let tempValue |
|||
let drawer |
|||
|
|||
const openDrawer = () => { |
|||
tempValue = componentInstance?._styles?.custom |
|||
drawer.show() |
|||
} |
|||
|
|||
const save = () => { |
|||
store.actions.components.updateCustomStyle(tempValue) |
|||
drawer.hide() |
|||
} |
|||
</script> |
|||
|
|||
<DetailSummary |
|||
name={`Custom CSS${componentInstance?._styles?.custom ? " *" : ""}`} |
|||
collapsible={false} |
|||
> |
|||
<div> |
|||
<ActionButton on:click={openDrawer}>Edit custom CSS</ActionButton> |
|||
</div> |
|||
</DetailSummary> |
|||
<Drawer bind:this={drawer} title="Custom CSS"> |
|||
<Button cta slot="buttons" on:click={save}>Save</Button> |
|||
<DrawerContent slot="body"> |
|||
<div class="content"> |
|||
<Layout gap="S"> |
|||
<Body size="S">Custom CSS overrides all other component styles.</Body> |
|||
<TextArea bind:value={tempValue} placeholder="Enter some CSS..." /> |
|||
</Layout> |
|||
</div> |
|||
</DrawerContent> |
|||
</Drawer> |
|||
|
|||
<style> |
|||
.content { |
|||
max-width: 800px; |
|||
margin: 0 auto; |
|||
} |
|||
.content :global(textarea) { |
|||
font-family: monospace; |
|||
min-height: 240px !important; |
|||
font-size: var(--font-size-s); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,34 @@ |
|||
<script> |
|||
import StyleSection from "./StyleSection.svelte" |
|||
import * as ComponentStyles from "./componentStyles" |
|||
|
|||
export let componentDefinition |
|||
export let componentInstance |
|||
|
|||
const getStyles = def => { |
|||
if (!def?.styles?.length) { |
|||
return [...ComponentStyles.all] |
|||
} |
|||
let styles = [...ComponentStyles.all] |
|||
def.styles.forEach(style => { |
|||
if (ComponentStyles[style]) { |
|||
styles.push(ComponentStyles[style]) |
|||
} |
|||
}) |
|||
return styles |
|||
} |
|||
|
|||
$: styles = getStyles(componentDefinition) |
|||
</script> |
|||
|
|||
{#if styles?.length > 0} |
|||
{#each styles as style} |
|||
<StyleSection |
|||
{style} |
|||
name={style.label} |
|||
columns={style.columns} |
|||
properties={style.settings} |
|||
{componentInstance} |
|||
/> |
|||
{/each} |
|||
{/if} |
|||
@ -1,111 +0,0 @@ |
|||
<script> |
|||
import { TextArea, DetailSummary, Button } from "@budibase/bbui" |
|||
import PropertyGroup from "./PropertyControls/PropertyGroup.svelte" |
|||
import FlatButtonGroup from "./PropertyControls/FlatButtonGroup" |
|||
import { allStyles } from "./componentStyles" |
|||
|
|||
export let componentDefinition = {} |
|||
export let componentInstance = {} |
|||
export let onStyleChanged = () => {} |
|||
export let onCustomStyleChanged = () => {} |
|||
export let onResetStyles = () => {} |
|||
|
|||
let selectedCategory = "normal" |
|||
let currentGroup |
|||
|
|||
function onChange(category) { |
|||
selectedCategory = category |
|||
} |
|||
|
|||
const buttonProps = [ |
|||
{ value: "normal", text: "Normal" }, |
|||
{ value: "hover", text: "Hover" }, |
|||
{ value: "active", text: "Active" }, |
|||
] |
|||
|
|||
$: groups = componentDefinition?.styleable ? Object.keys(allStyles) : [] |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<div class="state-categories"> |
|||
<FlatButtonGroup value={selectedCategory} {buttonProps} {onChange} /> |
|||
</div> |
|||
|
|||
<div class="positioned-wrapper"> |
|||
<div class="property-groups"> |
|||
{#if groups.length > 0} |
|||
{#each groups as groupName} |
|||
<PropertyGroup |
|||
name={groupName} |
|||
properties={allStyles[groupName]} |
|||
styleCategory={selectedCategory} |
|||
{onStyleChanged} |
|||
{componentInstance} |
|||
open={currentGroup === groupName} |
|||
on:open={() => (currentGroup = groupName)} |
|||
/> |
|||
{/each} |
|||
<DetailSummary |
|||
name={`Custom Styles${componentInstance._styles.custom ? " *" : ""}`} |
|||
on:open={() => (currentGroup = "custom")} |
|||
show={currentGroup === "custom"} |
|||
thin |
|||
> |
|||
<div class="custom-styles"> |
|||
<TextArea |
|||
value={componentInstance._styles.custom} |
|||
on:change={event => onCustomStyleChanged(event.detail)} |
|||
placeholder="Enter some CSS..." |
|||
/> |
|||
</div> |
|||
</DetailSummary> |
|||
<Button secondary wide on:click={onResetStyles}>Reset Styles</Button> |
|||
{:else} |
|||
<div class="no-design"> |
|||
This component doesn't have any design properties. |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
width: 100%; |
|||
height: 100%; |
|||
gap: var(--spacing-l); |
|||
} |
|||
|
|||
.positioned-wrapper { |
|||
position: relative; |
|||
display: flex; |
|||
min-height: 0; |
|||
flex: 1 1 auto; |
|||
} |
|||
|
|||
.property-groups { |
|||
flex: 1; |
|||
overflow-y: auto; |
|||
min-height: 0; |
|||
margin: 0 -20px; |
|||
padding: 0 20px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
gap: var(--spacing-m); |
|||
} |
|||
|
|||
.no-design { |
|||
font-size: var(--font-size-xs); |
|||
color: var(--grey-5); |
|||
} |
|||
|
|||
.custom-styles :global(textarea) { |
|||
font-family: monospace; |
|||
min-height: 120px; |
|||
font-size: var(--font-size-xs); |
|||
} |
|||
</style> |
|||
@ -1,84 +1,33 @@ |
|||
<script> |
|||
import { get } from "svelte/store" |
|||
import { store, selectedComponent, currentAsset } from "builderStore" |
|||
import { store, selectedComponent } from "builderStore" |
|||
import { Tabs, Tab } from "@budibase/bbui" |
|||
import { FrontendTypes } from "constants" |
|||
import DesignView from "./DesignView.svelte" |
|||
import SettingsView from "./SettingsView.svelte" |
|||
import { setWith } from "lodash" |
|||
import ScreenSettingsSection from "./ScreenSettingsSection.svelte" |
|||
import ComponentSettingsSection from "./ComponentSettingsSection.svelte" |
|||
import DesignSection from "./DesignSection.svelte" |
|||
import CustomStylesSection from "./CustomStylesSection.svelte" |
|||
|
|||
$: definition = store.actions.components.getDefinition( |
|||
$: componentInstance = $selectedComponent |
|||
$: componentDefinition = store.actions.components.getDefinition( |
|||
$selectedComponent?._component |
|||
) |
|||
$: isComponentOrScreen = |
|||
$store.currentView === "component" || |
|||
$store.currentFrontEndType === FrontendTypes.SCREEN |
|||
$: isNotScreenslot = !$selectedComponent._component.endsWith("screenslot") |
|||
$: showDisplayName = isComponentOrScreen && isNotScreenslot |
|||
|
|||
const onStyleChanged = store.actions.components.updateStyle |
|||
const onCustomStyleChanged = store.actions.components.updateCustomStyle |
|||
const onResetStyles = store.actions.components.resetStyles |
|||
|
|||
function setAssetProps(name, value) { |
|||
const selectedAsset = get(currentAsset) |
|||
store.update(state => { |
|||
if ( |
|||
name === "_instanceName" && |
|||
state.currentFrontEndType === FrontendTypes.SCREEN |
|||
) { |
|||
selectedAsset.props._instanceName = value |
|||
} else { |
|||
setWith(selectedAsset, name.split("."), value, Object) |
|||
} |
|||
return state |
|||
}) |
|||
store.actions.preview.saveSelected() |
|||
} |
|||
</script> |
|||
|
|||
<Tabs selected="Settings"> |
|||
<Tabs selected="Settings" noPadding> |
|||
<Tab title="Settings"> |
|||
<div class="tab-content-padding"> |
|||
{#if definition && definition.name} |
|||
<div class="instance-name">{definition.name}</div> |
|||
{/if} |
|||
<SettingsView |
|||
componentInstance={$selectedComponent} |
|||
componentDefinition={definition} |
|||
{showDisplayName} |
|||
onChange={store.actions.components.updateProp} |
|||
onScreenPropChange={setAssetProps} |
|||
assetInstance={$store.currentView !== "component" && $currentAsset} |
|||
/> |
|||
</div> |
|||
</Tab> |
|||
<Tab title="Design"> |
|||
<div class="tab-content-padding"> |
|||
{#if definition && definition.name} |
|||
<div class="instance-name">{definition.name}</div> |
|||
{/if} |
|||
<DesignView |
|||
componentInstance={$selectedComponent} |
|||
componentDefinition={definition} |
|||
{onStyleChanged} |
|||
{onCustomStyleChanged} |
|||
{onResetStyles} |
|||
/> |
|||
<div class="container"> |
|||
<ScreenSettingsSection {componentInstance} {componentDefinition} /> |
|||
<ComponentSettingsSection {componentInstance} {componentDefinition} /> |
|||
<DesignSection {componentInstance} {componentDefinition} /> |
|||
<CustomStylesSection {componentInstance} {componentDefinition} /> |
|||
</div> |
|||
</Tab> |
|||
</Tabs> |
|||
|
|||
<style> |
|||
.tab-content-padding { |
|||
padding: 0 var(--spacing-xl); |
|||
} |
|||
|
|||
.instance-name { |
|||
font-size: var(--spectrum-global-dimension-font-size-75); |
|||
margin-bottom: var(--spacing-m); |
|||
margin-top: var(--spacing-xs); |
|||
font-weight: 600; |
|||
color: var(--grey-7); |
|||
.container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
} |
|||
</style> |
|||
|
|||
@ -1,54 +0,0 @@ |
|||
<script> |
|||
import PropertyControl from "./PropertyControl.svelte" |
|||
import { DetailSummary } from "@budibase/bbui" |
|||
|
|||
export let name = "" |
|||
export let styleCategory = "normal" |
|||
export let properties = [] |
|||
export let componentInstance = {} |
|||
export let onStyleChanged = () => {} |
|||
export let open = false |
|||
|
|||
$: style = componentInstance["_styles"][styleCategory] || {} |
|||
$: changed = properties.some(prop => hasPropChanged(style, prop)) |
|||
|
|||
const hasPropChanged = (style, prop) => { |
|||
return style[prop.key] != null && style[prop.key] !== "" |
|||
} |
|||
|
|||
const getControlProps = props => { |
|||
let controlProps = { ...(props || {}) } |
|||
delete controlProps.label |
|||
delete controlProps.key |
|||
delete controlProps.control |
|||
return controlProps |
|||
} |
|||
</script> |
|||
|
|||
<DetailSummary name={`${name}${changed ? " *" : ""}`} on:open show={open} thin> |
|||
{#if open} |
|||
<div> |
|||
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)} |
|||
<PropertyControl |
|||
bindable={false} |
|||
label={`${prop.label}${hasPropChanged(style, prop) ? " *" : ""}`} |
|||
control={prop.control} |
|||
key={prop.key} |
|||
value={style[prop.key]} |
|||
onChange={value => onStyleChanged(styleCategory, prop.key, value)} |
|||
props={getControlProps(prop)} |
|||
/> |
|||
{/each} |
|||
</div> |
|||
{/if} |
|||
</DetailSummary> |
|||
|
|||
<style> |
|||
div { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
gap: var(--spacing-s); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,43 @@ |
|||
<script> |
|||
import { ActionButton } from "@budibase/bbui" |
|||
import { currentAsset, store } from "builderStore" |
|||
import { findClosestMatchingComponent } from "builderStore/storeUtils" |
|||
import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents" |
|||
import ConfirmDialog from "components/common/ConfirmDialog.svelte" |
|||
|
|||
export let componentInstance |
|||
|
|||
let confirmResetFieldsDialog |
|||
|
|||
const resetFormFields = () => { |
|||
const form = findClosestMatchingComponent( |
|||
$currentAsset.props, |
|||
componentInstance._id, |
|||
component => component._component.endsWith("/form") |
|||
) |
|||
const dataSource = form?.dataSource |
|||
const fields = makeDatasourceFormComponents(dataSource) |
|||
store.actions.components.updateProp( |
|||
"_children", |
|||
fields.map(field => field.json()) |
|||
) |
|||
} |
|||
</script> |
|||
|
|||
<div> |
|||
<ActionButton |
|||
secondary |
|||
wide |
|||
on:click={() => confirmResetFieldsDialog?.show()} |
|||
> |
|||
Update form fields |
|||
</ActionButton> |
|||
</div> |
|||
|
|||
<ConfirmDialog |
|||
bind:this={confirmResetFieldsDialog} |
|||
body={`All components inside this group will be deleted and replaced with fields to match the schema. Are you sure you want to update this Field Group?`} |
|||
okText="Update" |
|||
onOk={resetFormFields} |
|||
title="Confirm Form Field Update" |
|||
/> |
|||
@ -0,0 +1,50 @@ |
|||
<script> |
|||
import { get } from "svelte/store" |
|||
import { get as deepGet, setWith } from "lodash" |
|||
import { Input, DetailSummary } from "@budibase/bbui" |
|||
import PropertyControl from "./PropertyControls/PropertyControl.svelte" |
|||
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte" |
|||
import RoleSelect from "./PropertyControls/RoleSelect.svelte" |
|||
import { currentAsset, store } from "builderStore" |
|||
import { FrontendTypes } from "constants" |
|||
|
|||
export let componentInstance |
|||
|
|||
function setAssetProps(name, value) { |
|||
const selectedAsset = get(currentAsset) |
|||
store.update(state => { |
|||
if ( |
|||
name === "_instanceName" && |
|||
state.currentFrontEndType === FrontendTypes.SCREEN |
|||
) { |
|||
selectedAsset.props._instanceName = value |
|||
} else { |
|||
setWith(selectedAsset, name.split("."), value, Object) |
|||
} |
|||
return state |
|||
}) |
|||
store.actions.preview.saveSelected() |
|||
} |
|||
|
|||
const screenSettings = [ |
|||
// { key: "description", label: "Description", control: Input }, |
|||
{ key: "routing.route", label: "Route", control: Input }, |
|||
{ key: "routing.roleId", label: "Access", control: RoleSelect }, |
|||
{ key: "layoutId", label: "Layout", control: LayoutSelect }, |
|||
] |
|||
</script> |
|||
|
|||
{#if $store.currentView !== "component" && $currentAsset && $store.currentFrontEndType === FrontendTypes.SCREEN} |
|||
<DetailSummary name="Screen" collapsible={false}> |
|||
{#each screenSettings as def (`${componentInstance._id}-${def.key}`)} |
|||
<PropertyControl |
|||
bindable={false} |
|||
control={def.control} |
|||
label={def.label} |
|||
key={def.key} |
|||
value={deepGet($currentAsset, def.key)} |
|||
onChange={val => setAssetProps(def.key, val)} |
|||
/> |
|||
{/each} |
|||
</DetailSummary> |
|||
{/if} |
|||
@ -0,0 +1,51 @@ |
|||
<script> |
|||
import PropertyControl from "./PropertyControls/PropertyControl.svelte" |
|||
import { DetailSummary } from "@budibase/bbui" |
|||
import { store } from "builderStore" |
|||
|
|||
export let name |
|||
export let columns |
|||
export let properties |
|||
export let componentInstance |
|||
|
|||
$: style = componentInstance._styles.normal || {} |
|||
$: changed = properties?.some(prop => hasPropChanged(style, prop)) ?? false |
|||
|
|||
const hasPropChanged = (style, prop) => { |
|||
return style[prop.key] != null && style[prop.key] !== "" |
|||
} |
|||
|
|||
const getControlProps = props => { |
|||
let controlProps = { ...(props || {}) } |
|||
delete controlProps.label |
|||
delete controlProps.key |
|||
delete controlProps.control |
|||
return controlProps |
|||
} |
|||
</script> |
|||
|
|||
<DetailSummary collapsible={false} name={`${name}${changed ? " *" : ""}`}> |
|||
<div class="group-content" style="grid-template-columns: {columns || '1fr'}"> |
|||
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)} |
|||
<div style="grid-column: {prop.column || 'auto'}"> |
|||
<PropertyControl |
|||
bindable={false} |
|||
label={`${prop.label}${hasPropChanged(style, prop) ? " *" : ""}`} |
|||
control={prop.control} |
|||
key={prop.key} |
|||
value={style[prop.key]} |
|||
onChange={val => store.actions.components.updateStyle(prop.key, val)} |
|||
props={getControlProps(prop)} |
|||
/> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
</DetailSummary> |
|||
|
|||
<style> |
|||
.group-content { |
|||
display: grid; |
|||
align-items: stretch; |
|||
gap: var(--spacing-l); |
|||
} |
|||
</style> |
|||
File diff suppressed because it is too large
@ -0,0 +1,26 @@ |
|||
<script> |
|||
import { ColorPicker } from "@budibase/bbui" |
|||
import { builderStore } from "../../store" |
|||
|
|||
export let prop |
|||
|
|||
$: currentValue = $builderStore.selectedComponent?.[prop] |
|||
</script> |
|||
|
|||
<div> |
|||
<ColorPicker |
|||
size="S" |
|||
value={currentValue} |
|||
on:change={e => { |
|||
if (prop) { |
|||
builderStore.actions.updateProp(prop, e.detail) |
|||
} |
|||
}} |
|||
/> |
|||
</div> |
|||
|
|||
<style> |
|||
div { |
|||
padding: 0 4px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,31 @@ |
|||
<script> |
|||
import { Select } from "@budibase/bbui" |
|||
import { builderStore } from "../../store" |
|||
|
|||
export let prop |
|||
export let options |
|||
export let label |
|||
|
|||
$: currentValue = $builderStore.selectedComponent?.[prop] |
|||
</script> |
|||
|
|||
<div> |
|||
<Select |
|||
quiet |
|||
autoWidth |
|||
placeholder={label} |
|||
{options} |
|||
value={currentValue} |
|||
on:change={e => { |
|||
if (prop) { |
|||
builderStore.actions.updateProp(prop, e.detail) |
|||
} |
|||
}} |
|||
/> |
|||
</div> |
|||
|
|||
<style> |
|||
div { |
|||
padding: 0 4px; |
|||
} |
|||
</style> |
|||
@ -1,23 +1,29 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
import Placeholder from "./Placeholder.svelte" |
|||
|
|||
const { styleable } = getContext("sdk") |
|||
const { styleable, builderStore } = getContext("sdk") |
|||
const component = getContext("component") |
|||
|
|||
export let embed |
|||
</script> |
|||
|
|||
<div use:styleable={$component.styles}> |
|||
{@html embed} |
|||
</div> |
|||
{#if embed} |
|||
<div class="embed" use:styleable={$component.styles}> |
|||
{@html embed} |
|||
</div> |
|||
{:else if $builderStore.inBuilder} |
|||
<div use:styleable={{ ...$component.styles, empty: true }}> |
|||
<Placeholder /> |
|||
</div> |
|||
{/if} |
|||
|
|||
<style> |
|||
div { |
|||
.embed { |
|||
position: relative; |
|||
} |
|||
div :global(> *) { |
|||
.embed :global(> *) { |
|||
width: 100%; |
|||
height: 100%; |
|||
position: absolute; |
|||
} |
|||
</style> |
|||
|
|||
@ -1,21 +1,27 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
import Placeholder from "./Placeholder.svelte" |
|||
|
|||
const { styleable } = getContext("sdk") |
|||
const { styleable, builderStore } = getContext("sdk") |
|||
const component = getContext("component") |
|||
|
|||
export let className = "" |
|||
export let url = "" |
|||
export let description = "" |
|||
export let height |
|||
export let width |
|||
export let url |
|||
</script> |
|||
|
|||
<img |
|||
{height} |
|||
{width} |
|||
class={className} |
|||
src={url} |
|||
alt={description} |
|||
use:styleable={$component.styles} |
|||
/> |
|||
{#if url} |
|||
<img src={url} alt={$component.name} use:styleable={$component.styles} /> |
|||
{:else if $builderStore.inBuilder} |
|||
<div |
|||
class="placeholder" |
|||
use:styleable={{ ...$component.styles, empty: true }} |
|||
> |
|||
<Placeholder /> |
|||
</div> |
|||
{/if} |
|||
|
|||
<style> |
|||
.placeholder { |
|||
display: grid; |
|||
place-items: center; |
|||
} |
|||
</style> |
|||
|
|||
@ -1,31 +1,105 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
|
|||
const { linkable, styleable } = getContext("sdk") |
|||
const { linkable, styleable, builderStore } = getContext("sdk") |
|||
const component = getContext("component") |
|||
|
|||
export let url = "" |
|||
export let text = "" |
|||
export let openInNewTab = false |
|||
export let external = false |
|||
export let url |
|||
export let text |
|||
export let openInNewTab |
|||
export let color |
|||
export let align |
|||
export let bold |
|||
export let italic |
|||
export let underline |
|||
export let size |
|||
|
|||
$: external = url && !url.startsWith("/") |
|||
$: target = openInNewTab ? "_blank" : "_self" |
|||
$: placeholder = $builderStore.inBuilder && !text |
|||
$: componentText = $builderStore.inBuilder |
|||
? text || "Placeholder link" |
|||
: text || "" |
|||
|
|||
// Add color styles to main styles object, otherwise the styleable helper |
|||
// overrides the color when it's passed as inline style. |
|||
$: styles = { |
|||
...$component.styles, |
|||
normal: { |
|||
...$component.styles?.normal, |
|||
color, |
|||
}, |
|||
} |
|||
</script> |
|||
|
|||
{#if external} |
|||
<a href={url || "/"} {target} use:styleable={$component.styles}> |
|||
{text} |
|||
<slot /> |
|||
</a> |
|||
{:else} |
|||
<a href={url || "/"} use:linkable {target} use:styleable={$component.styles}> |
|||
{text} |
|||
<slot /> |
|||
</a> |
|||
{#if $builderStore.inBuilder || componentText} |
|||
{#if external} |
|||
<a |
|||
{target} |
|||
href={url || "/"} |
|||
use:styleable={styles} |
|||
class:placeholder |
|||
class:bold |
|||
class:italic |
|||
class:underline |
|||
class="align--{align || 'left'} size--{size || 'M'}" |
|||
> |
|||
{componentText} |
|||
</a> |
|||
{:else} |
|||
<a |
|||
use:linkable |
|||
href={url || "/"} |
|||
use:styleable={styles} |
|||
class:placeholder |
|||
class:bold |
|||
class:italic |
|||
class:underline |
|||
class="align--{align || 'left'} size--{size || 'M'}" |
|||
> |
|||
{componentText} |
|||
</a> |
|||
{/if} |
|||
{/if} |
|||
|
|||
<style> |
|||
a { |
|||
color: var(--spectrum-alias-text-color); |
|||
display: inline-block; |
|||
white-space: pre-wrap; |
|||
} |
|||
.placeholder { |
|||
font-style: italic; |
|||
color: var(--grey-6); |
|||
} |
|||
.bold { |
|||
font-weight: 600; |
|||
} |
|||
.italic { |
|||
font-style: italic; |
|||
} |
|||
.underline { |
|||
text-decoration: underline; |
|||
} |
|||
.size--S { |
|||
font-size: 14px; |
|||
} |
|||
.size--M { |
|||
font-size: 16px; |
|||
} |
|||
.size--L { |
|||
font-size: 18px; |
|||
} |
|||
.align--left { |
|||
text-align: left; |
|||
} |
|||
.align--center { |
|||
text-align: center; |
|||
} |
|||
.align--right { |
|||
text-align: right; |
|||
} |
|||
.align-justify { |
|||
text-align: justify; |
|||
} |
|||
</style> |
|||
|
|||
Loading…
Reference in new issue