Merge pull request #7015 from Budibase/cheeks-lab-day-keyboard-shortcuts-develop
Keyboard shortcuts for components
This commit is contained in:
commit
7c394f5876
21 changed files with 574 additions and 271 deletions
|
@ -83,4 +83,9 @@
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spectrum-Icon--sizeXS {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher, getContext } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
|
import Icon from "../Icon/Icon.svelte"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const actionMenu = getContext("actionMenu")
|
const actionMenu = getContext("actionMenu")
|
||||||
|
@ -8,6 +9,22 @@
|
||||||
export let icon = undefined
|
export let icon = undefined
|
||||||
export let disabled = undefined
|
export let disabled = undefined
|
||||||
export let noClose = false
|
export let noClose = false
|
||||||
|
export let keyBind = undefined
|
||||||
|
|
||||||
|
$: keys = getKeys(keyBind)
|
||||||
|
|
||||||
|
const getKeys = keyBind => {
|
||||||
|
let keys = keyBind?.split("+") || []
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
if (
|
||||||
|
keys[i].toLowerCase() === "ctrl" &&
|
||||||
|
navigator.platform.startsWith("Mac")
|
||||||
|
) {
|
||||||
|
keys[i] = "⌘"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
if (actionMenu && !noClose) {
|
if (actionMenu && !noClose) {
|
||||||
|
@ -26,20 +43,54 @@
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<svg
|
<div class="icon">
|
||||||
class="spectrum-Icon spectrum-Icon--sizeS spectrum-Menu-itemIcon"
|
<Icon name={icon} size="S" />
|
||||||
focusable="false"
|
</div>
|
||||||
aria-hidden="true"
|
|
||||||
aria-label={icon}
|
|
||||||
>
|
|
||||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
|
||||||
</svg>
|
|
||||||
{/if}
|
{/if}
|
||||||
<span class="spectrum-Menu-itemLabel"><slot /></span>
|
<span class="spectrum-Menu-itemLabel"><slot /></span>
|
||||||
|
{#if keys?.length}
|
||||||
|
<div class="keys">
|
||||||
|
{#each keys as key}
|
||||||
|
<div class="key">
|
||||||
|
{#if key.startsWith("!")}
|
||||||
|
<Icon size="XS" name={key.split("!")[1]} />
|
||||||
|
{:else}
|
||||||
|
{key}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.spectrum-Menu-itemIcon {
|
.icon {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.keys {
|
||||||
|
margin-left: 30px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.key {
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: var(--spectrum-global-color-gray-300);
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 12px;
|
||||||
|
height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
margin: -1px 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
.is-disabled .key {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let visible = fixed || inline
|
let visible = fixed || inline
|
||||||
|
let modal
|
||||||
|
|
||||||
$: dispatch(visible ? "show" : "hide")
|
$: dispatch(visible ? "show" : "hide")
|
||||||
|
|
||||||
export function show() {
|
export function show() {
|
||||||
|
@ -41,12 +43,22 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function focusFirstInput(node) {
|
async function focusModal(node) {
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
// Try to focus first input
|
||||||
const inputs = node.querySelectorAll("input")
|
const inputs = node.querySelectorAll("input")
|
||||||
if (inputs?.length) {
|
if (inputs?.length) {
|
||||||
await tick()
|
|
||||||
inputs[0].focus()
|
inputs[0].focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Otherwise try to focus confirmation button
|
||||||
|
else if (modal) {
|
||||||
|
const confirm = modal.querySelector(".confirm-wrap .spectrum-Button")
|
||||||
|
if (confirm) {
|
||||||
|
confirm.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setContext(Context.Modal, { show, hide, cancel })
|
setContext(Context.Modal, { show, hide, cancel })
|
||||||
|
@ -56,7 +68,7 @@
|
||||||
|
|
||||||
{#if inline}
|
{#if inline}
|
||||||
{#if visible}
|
{#if visible}
|
||||||
<div use:focusFirstInput class="spectrum-Modal inline is-open">
|
<div use:focusModal bind:this={modal} class="spectrum-Modal inline is-open">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -70,17 +82,18 @@
|
||||||
-->
|
-->
|
||||||
<Portal target=".modal-container">
|
<Portal target=".modal-container">
|
||||||
{#if visible}
|
{#if visible}
|
||||||
<div
|
<div class="spectrum-Underlay is-open" on:mousedown|self={cancel}>
|
||||||
class="spectrum-Underlay is-open"
|
<div
|
||||||
in:fade={{ duration: 200 }}
|
class="background"
|
||||||
out:fade|local={{ duration: 200 }}
|
in:fade={{ duration: 200 }}
|
||||||
on:mousedown|self={cancel}
|
out:fade|local={{ duration: 200 }}
|
||||||
>
|
/>
|
||||||
<div class="modal-wrapper" on:mousedown|self={cancel}>
|
<div class="modal-wrapper" on:mousedown|self={cancel}>
|
||||||
<div class="modal-inner-wrapper" on:mousedown|self={cancel}>
|
<div class="modal-inner-wrapper" on:mousedown|self={cancel}>
|
||||||
<slot name="outside" />
|
<slot name="outside" />
|
||||||
<div
|
<div
|
||||||
use:focusFirstInput
|
use:focusModal
|
||||||
|
bind:this={modal}
|
||||||
class="spectrum-Modal is-open"
|
class="spectrum-Modal is-open"
|
||||||
in:fly={{ y: 30, duration: 200 }}
|
in:fly={{ y: 30, duration: 200 }}
|
||||||
out:fly|local={{ y: 30, duration: 200 }}
|
out:fly|local={{ y: 30, duration: 200 }}
|
||||||
|
@ -103,7 +116,17 @@
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background: rgba(0, 0, 0, 0.75);
|
background: transparent;
|
||||||
|
}
|
||||||
|
.background {
|
||||||
|
background: var(--modal-background, rgba(0, 0, 0, 0.75));
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
opacity: 0.65;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-wrapper {
|
.modal-wrapper {
|
||||||
|
|
|
@ -534,7 +534,16 @@ export const getFrontendStore = () => {
|
||||||
if (!component) {
|
if (!component) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let parentId
|
|
||||||
|
// Determine the next component to select after deletion
|
||||||
|
const state = get(store)
|
||||||
|
let nextSelectedComponentId
|
||||||
|
if (state.selectedComponentId === component._id) {
|
||||||
|
nextSelectedComponentId = store.actions.components.getNext()
|
||||||
|
if (!nextSelectedComponentId) {
|
||||||
|
nextSelectedComponentId = store.actions.components.getPrevious()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Patch screen
|
// Patch screen
|
||||||
await store.actions.screens.patch(screen => {
|
await store.actions.screens.patch(screen => {
|
||||||
|
@ -549,17 +558,18 @@ export const getFrontendStore = () => {
|
||||||
if (!parent) {
|
if (!parent) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
parentId = parent._id
|
|
||||||
parent._children = parent._children.filter(
|
parent._children = parent._children.filter(
|
||||||
child => child._id !== component._id
|
child => child._id !== component._id
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Select the deleted component's parent
|
// Update selected component if required
|
||||||
store.update(state => {
|
if (nextSelectedComponentId) {
|
||||||
state.selectedComponentId = parentId
|
store.update(state => {
|
||||||
return state
|
state.selectedComponentId = nextSelectedComponentId
|
||||||
})
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
copy: (component, cut = false, selectParent = true) => {
|
copy: (component, cut = false, selectParent = true) => {
|
||||||
// Update store with copied component
|
// Update store with copied component
|
||||||
|
@ -618,6 +628,16 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check inside is valid
|
||||||
|
if (mode === "inside") {
|
||||||
|
const definition = store.actions.components.getDefinition(
|
||||||
|
targetComponent._component
|
||||||
|
)
|
||||||
|
if (!definition.hasChildren) {
|
||||||
|
mode = "below"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Paste new component
|
// Paste new component
|
||||||
if (mode === "inside") {
|
if (mode === "inside") {
|
||||||
// Paste inside target component if chosen
|
// Paste inside target component if chosen
|
||||||
|
@ -654,46 +674,193 @@ export const getFrontendStore = () => {
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
getPrevious: () => {
|
||||||
|
const state = get(store)
|
||||||
|
const componentId = state.selectedComponentId
|
||||||
|
const screen = get(selectedScreen)
|
||||||
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
|
|
||||||
|
// Check we aren't right at the top of the tree
|
||||||
|
const index = parent?._children.findIndex(x => x._id === componentId)
|
||||||
|
if (!parent || componentId === screen.props._id) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have siblings above us, choose the sibling or a descendant
|
||||||
|
if (index > 0) {
|
||||||
|
// If sibling before us accepts children, select a descendant
|
||||||
|
const previousSibling = parent._children[index - 1]
|
||||||
|
if (previousSibling._children?.length) {
|
||||||
|
let target = previousSibling
|
||||||
|
while (target._children?.length) {
|
||||||
|
target = target._children[target._children.length - 1]
|
||||||
|
}
|
||||||
|
return target._id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise just select sibling
|
||||||
|
return previousSibling._id
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no siblings above us, select the parent
|
||||||
|
return parent._id
|
||||||
|
},
|
||||||
|
getNext: () => {
|
||||||
|
const component = get(selectedComponent)
|
||||||
|
const componentId = component?._id
|
||||||
|
const screen = get(selectedScreen)
|
||||||
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
|
const index = parent?._children.findIndex(x => x._id === componentId)
|
||||||
|
|
||||||
|
// If we have children, select first child
|
||||||
|
if (component._children?.length) {
|
||||||
|
return component._children[0]._id
|
||||||
|
} else if (!parent) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise select the next sibling if we have one
|
||||||
|
if (index < parent._children.length - 1) {
|
||||||
|
const nextSibling = parent._children[index + 1]
|
||||||
|
return nextSibling._id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last child, select our parents next sibling
|
||||||
|
let target = parent
|
||||||
|
let targetParent = findComponentParent(screen.props, target._id)
|
||||||
|
let targetIndex = targetParent?._children.findIndex(
|
||||||
|
child => child._id === target._id
|
||||||
|
)
|
||||||
|
while (
|
||||||
|
targetParent != null &&
|
||||||
|
targetIndex === targetParent._children?.length - 1
|
||||||
|
) {
|
||||||
|
target = targetParent
|
||||||
|
targetParent = findComponentParent(screen.props, target._id)
|
||||||
|
targetIndex = targetParent?._children.findIndex(
|
||||||
|
child => child._id === target._id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (targetParent) {
|
||||||
|
return targetParent._children[targetIndex + 1]._id
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectPrevious: () => {
|
||||||
|
const previousId = store.actions.components.getPrevious()
|
||||||
|
if (previousId) {
|
||||||
|
store.update(state => {
|
||||||
|
state.selectedComponentId = previousId
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectNext: () => {
|
||||||
|
const nextId = store.actions.components.getNext()
|
||||||
|
if (nextId) {
|
||||||
|
store.update(state => {
|
||||||
|
state.selectedComponentId = nextId
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
moveUp: async component => {
|
moveUp: async component => {
|
||||||
await store.actions.screens.patch(screen => {
|
await store.actions.screens.patch(screen => {
|
||||||
const componentId = component?._id
|
const componentId = component?._id
|
||||||
const parent = findComponentParent(screen.props, componentId)
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
if (!parent?._children?.length) {
|
|
||||||
return false
|
// Check we aren't right at the top of the tree
|
||||||
|
const index = parent?._children.findIndex(x => x._id === componentId)
|
||||||
|
if (!parent || (index === 0 && parent._id === screen.props._id)) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
const currentIndex = parent._children.findIndex(
|
|
||||||
child => child._id === componentId
|
// Copy original component and remove it from the parent
|
||||||
)
|
const originalComponent = cloneDeep(parent._children[index])
|
||||||
if (currentIndex === 0) {
|
parent._children = parent._children.filter(
|
||||||
return false
|
|
||||||
}
|
|
||||||
const originalComponent = cloneDeep(parent._children[currentIndex])
|
|
||||||
const newChildren = parent._children.filter(
|
|
||||||
component => component._id !== componentId
|
component => component._id !== componentId
|
||||||
)
|
)
|
||||||
newChildren.splice(currentIndex - 1, 0, originalComponent)
|
|
||||||
parent._children = newChildren
|
// If we have siblings above us, move up
|
||||||
|
if (index > 0) {
|
||||||
|
// If sibling before us accepts children, move to last child of
|
||||||
|
// sibling
|
||||||
|
const previousSibling = parent._children[index - 1]
|
||||||
|
const definition = store.actions.components.getDefinition(
|
||||||
|
previousSibling._component
|
||||||
|
)
|
||||||
|
if (definition.hasChildren) {
|
||||||
|
previousSibling._children.push(originalComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise just move component above sibling
|
||||||
|
else {
|
||||||
|
parent._children.splice(index - 1, 0, originalComponent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no siblings above us, go above the parent as long as it isn't
|
||||||
|
// the screen
|
||||||
|
else if (parent._id !== screen.props._id) {
|
||||||
|
const grandParent = findComponentParent(screen.props, parent._id)
|
||||||
|
const parentIndex = grandParent._children.findIndex(
|
||||||
|
child => child._id === parent._id
|
||||||
|
)
|
||||||
|
grandParent._children.splice(parentIndex, 0, originalComponent)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
moveDown: async component => {
|
moveDown: async component => {
|
||||||
await store.actions.screens.patch(screen => {
|
await store.actions.screens.patch(screen => {
|
||||||
const componentId = component?._id
|
const componentId = component?._id
|
||||||
const parent = findComponentParent(screen.props, componentId)
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
|
|
||||||
|
// Sanity check parent is found
|
||||||
if (!parent?._children?.length) {
|
if (!parent?._children?.length) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const currentIndex = parent._children.findIndex(
|
|
||||||
child => child._id === componentId
|
// Check we aren't right at the bottom of the tree
|
||||||
)
|
const index = parent._children.findIndex(x => x._id === componentId)
|
||||||
if (currentIndex === parent._children.length - 1) {
|
if (
|
||||||
return false
|
index === parent._children.length - 1 &&
|
||||||
|
parent._id === screen.props._id
|
||||||
|
) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
const originalComponent = cloneDeep(parent._children[currentIndex])
|
|
||||||
const newChildren = parent._children.filter(
|
// Copy the original component and remove from parent
|
||||||
|
const originalComponent = cloneDeep(parent._children[index])
|
||||||
|
parent._children = parent._children.filter(
|
||||||
component => component._id !== componentId
|
component => component._id !== componentId
|
||||||
)
|
)
|
||||||
newChildren.splice(currentIndex + 1, 0, originalComponent)
|
|
||||||
parent._children = newChildren
|
// Move below the next sibling if we are not the last sibling
|
||||||
|
if (index < parent._children.length) {
|
||||||
|
// If the next sibling has children, become the first child
|
||||||
|
const nextSibling = parent._children[index]
|
||||||
|
const definition = store.actions.components.getDefinition(
|
||||||
|
nextSibling._component
|
||||||
|
)
|
||||||
|
if (definition.hasChildren) {
|
||||||
|
nextSibling._children.splice(0, 0, originalComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise move below next sibling
|
||||||
|
else {
|
||||||
|
parent._children.splice(index + 1, 0, originalComponent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last child, so move below our parent
|
||||||
|
else {
|
||||||
|
const grandParent = findComponentParent(screen.props, parent._id)
|
||||||
|
const parentIndex = grandParent._children.findIndex(
|
||||||
|
child => child._id === parent._id
|
||||||
|
)
|
||||||
|
grandParent._children.splice(parentIndex + 1, 0, originalComponent)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateStyle: async (name, value) => {
|
updateStyle: async (name, value) => {
|
||||||
|
|
|
@ -162,7 +162,7 @@
|
||||||
width="28px"
|
width="28px"
|
||||||
height="28px"
|
height="28px"
|
||||||
class="spectrum-Icon"
|
class="spectrum-Icon"
|
||||||
style="color:grey;"
|
style="color:var(--spectrum-global-color-gray-700);"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
>
|
>
|
||||||
<use xlink:href="#spectrum-icon-18-Reuse" />
|
<use xlink:href="#spectrum-icon-18-Reuse" />
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
width="28px"
|
width="28px"
|
||||||
height="28px"
|
height="28px"
|
||||||
class="spectrum-Icon"
|
class="spectrum-Icon"
|
||||||
style="color:grey;"
|
style="color:var(--spectrum-global-color-gray-700);"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
>
|
>
|
||||||
<use xlink:href="#spectrum-icon-18-{block.icon}" />
|
<use xlink:href="#spectrum-icon-18-{block.icon}" />
|
||||||
|
|
|
@ -151,7 +151,11 @@
|
||||||
} else if (type === "update-prop") {
|
} else if (type === "update-prop") {
|
||||||
await store.actions.components.updateSetting(data.prop, data.value)
|
await store.actions.components.updateSetting(data.prop, data.value)
|
||||||
} else if (type === "delete-component" && data.id) {
|
} else if (type === "delete-component" && data.id) {
|
||||||
|
// Legacy type, can be deleted in future
|
||||||
confirmDeleteComponent(data.id)
|
confirmDeleteComponent(data.id)
|
||||||
|
} else if (type === "key-down") {
|
||||||
|
const { key, ctrlKey } = data
|
||||||
|
document.dispatchEvent(new KeyboardEvent("keydown", { key, ctrlKey }))
|
||||||
} else if (type === "duplicate-component" && data.id) {
|
} else if (type === "duplicate-component" && data.id) {
|
||||||
const rootComponent = get(currentAsset).props
|
const rootComponent = get(currentAsset).props
|
||||||
const component = findComponent(rootComponent, data.id)
|
const component = findComponent(rootComponent, data.id)
|
||||||
|
|
|
@ -1,117 +1,69 @@
|
||||||
<script>
|
<script>
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
|
||||||
import { ActionMenu, MenuItem, Icon, notifications } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export let component
|
|
||||||
|
|
||||||
let confirmDeleteDialog
|
|
||||||
|
|
||||||
$: definition = store.actions.components.getDefinition(component?._component)
|
|
||||||
$: noChildrenAllowed = !component || !definition?.hasChildren
|
|
||||||
$: noPaste = !$store.componentToPaste
|
$: noPaste = !$store.componentToPaste
|
||||||
|
|
||||||
// "editable" has been repurposed for inline text editing.
|
const keyboardEvent = (key, ctrlKey = false) => {
|
||||||
// It remains here for legacy compatibility.
|
document.dispatchEvent(new KeyboardEvent("keydown", { key, ctrlKey }))
|
||||||
// Future components should define "static": true for indicate they should
|
|
||||||
// not show a context menu.
|
|
||||||
$: showMenu = definition?.editable !== false && definition?.static !== true
|
|
||||||
|
|
||||||
const moveUpComponent = async () => {
|
|
||||||
try {
|
|
||||||
await store.actions.components.moveUp(component)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error moving component up")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const moveDownComponent = async () => {
|
|
||||||
try {
|
|
||||||
await store.actions.components.moveDown(component)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error moving component down")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const duplicateComponent = () => {
|
|
||||||
storeComponentForCopy(false)
|
|
||||||
pasteComponent("below")
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteComponent = async () => {
|
|
||||||
try {
|
|
||||||
await store.actions.components.delete(component)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error deleting component")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const storeComponentForCopy = (cut = false) => {
|
|
||||||
store.actions.components.copy(component, cut)
|
|
||||||
}
|
|
||||||
|
|
||||||
const pasteComponent = mode => {
|
|
||||||
try {
|
|
||||||
store.actions.components.paste(component, mode)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error saving component")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showMenu}
|
<ActionMenu>
|
||||||
<ActionMenu>
|
<div slot="control" class="icon">
|
||||||
<div slot="control" class="icon">
|
<Icon size="S" hoverable name="MoreSmallList" />
|
||||||
<Icon size="S" hoverable name="MoreSmallList" />
|
</div>
|
||||||
</div>
|
<MenuItem
|
||||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>
|
icon="Delete"
|
||||||
Delete
|
keyBind="!BackAndroid"
|
||||||
</MenuItem>
|
on:click={() => keyboardEvent("Delete")}
|
||||||
<MenuItem noClose icon="ChevronUp" on:click={moveUpComponent}>
|
>
|
||||||
Move up
|
Delete
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem noClose icon="ChevronDown" on:click={moveDownComponent}>
|
<MenuItem
|
||||||
Move down
|
icon="ChevronUp"
|
||||||
</MenuItem>
|
keyBind="Ctrl+!ArrowUp"
|
||||||
<MenuItem noClose icon="Duplicate" on:click={duplicateComponent}>
|
on:click={() => keyboardEvent("ArrowUp", true)}
|
||||||
Duplicate
|
>
|
||||||
</MenuItem>
|
Move up
|
||||||
<MenuItem icon="Cut" on:click={() => storeComponentForCopy(true)}>
|
</MenuItem>
|
||||||
Cut
|
<MenuItem
|
||||||
</MenuItem>
|
icon="ChevronDown"
|
||||||
<MenuItem icon="Copy" on:click={() => storeComponentForCopy(false)}>
|
keyBind="Ctrl+!ArrowDown"
|
||||||
Copy
|
on:click={() => keyboardEvent("ArrowDown", true)}
|
||||||
</MenuItem>
|
>
|
||||||
<MenuItem
|
Move down
|
||||||
icon="LayersBringToFront"
|
</MenuItem>
|
||||||
on:click={() => pasteComponent("above")}
|
<MenuItem
|
||||||
disabled={noPaste}
|
icon="Duplicate"
|
||||||
>
|
keyBind="Ctrl+D"
|
||||||
Paste above
|
on:click={() => keyboardEvent("d", true)}
|
||||||
</MenuItem>
|
>
|
||||||
<MenuItem
|
Duplicate
|
||||||
icon="LayersSendToBack"
|
</MenuItem>
|
||||||
on:click={() => pasteComponent("below")}
|
<MenuItem
|
||||||
disabled={noPaste}
|
icon="Cut"
|
||||||
>
|
keyBind="Ctrl+X"
|
||||||
Paste below
|
on:click={() => keyboardEvent("x", true)}
|
||||||
</MenuItem>
|
>
|
||||||
<MenuItem
|
Cut
|
||||||
icon="ShowOneLayer"
|
</MenuItem>
|
||||||
on:click={() => pasteComponent("inside")}
|
<MenuItem
|
||||||
disabled={noPaste || noChildrenAllowed}
|
icon="Copy"
|
||||||
>
|
keyBind="Ctrl+C"
|
||||||
Paste inside
|
on:click={() => keyboardEvent("c", true)}
|
||||||
</MenuItem>
|
>
|
||||||
</ActionMenu>
|
Copy
|
||||||
<ConfirmDialog
|
</MenuItem>
|
||||||
bind:this={confirmDeleteDialog}
|
<MenuItem
|
||||||
title="Confirm Deletion"
|
icon="LayersSendToBack"
|
||||||
body={`Are you sure you wish to delete this '${definition?.name}' component?`}
|
keyBind="Ctrl+V"
|
||||||
okText="Delete Component"
|
on:click={() => keyboardEvent("v", true)}
|
||||||
onOk={deleteComponent}
|
disabled={noPaste}
|
||||||
/>
|
>
|
||||||
{/if}
|
Paste
|
||||||
|
</MenuItem>
|
||||||
|
</ActionMenu>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.icon {
|
.icon {
|
||||||
|
|
|
@ -2,16 +2,19 @@
|
||||||
import Panel from "components/design/Panel.svelte"
|
import Panel from "components/design/Panel.svelte"
|
||||||
import ComponentTree from "./ComponentTree.svelte"
|
import ComponentTree from "./ComponentTree.svelte"
|
||||||
import { dndStore } from "./dndStore.js"
|
import { dndStore } from "./dndStore.js"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto, isActive } from "@roxi/routify"
|
||||||
import { store, selectedScreen } from "builderStore"
|
import { store, selectedScreen, selectedComponent } from "builderStore"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
|
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
|
||||||
import { setContext } from "svelte"
|
import { setContext, onMount } from "svelte"
|
||||||
|
import { get } from "svelte/store"
|
||||||
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
||||||
import { DropPosition } from "./dndStore"
|
import { DropPosition } from "./dndStore"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { notifications, Button } from "@budibase/bbui"
|
import { notifications, Button } from "@budibase/bbui"
|
||||||
|
|
||||||
let scrollRef
|
let scrollRef
|
||||||
|
let confirmDeleteDialog
|
||||||
|
|
||||||
const scrollTo = bounds => {
|
const scrollTo = bounds => {
|
||||||
if (!bounds) {
|
if (!bounds) {
|
||||||
|
@ -69,6 +72,76 @@
|
||||||
setContext("scroll", {
|
setContext("scroll", {
|
||||||
scrollTo,
|
scrollTo,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const deleteComponent = async () => {
|
||||||
|
await store.actions.components.delete(get(selectedComponent))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyPress = async e => {
|
||||||
|
// Ignore repeating events
|
||||||
|
if (e.repeat) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Ignore events when typing
|
||||||
|
const activeTag = document.activeElement?.tagName.toLowerCase()
|
||||||
|
if (["input", "textarea"].indexOf(activeTag) !== -1 && e.key !== "Escape") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const component = get(selectedComponent)
|
||||||
|
try {
|
||||||
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault()
|
||||||
|
await store.actions.components.moveUp(component)
|
||||||
|
} else if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault()
|
||||||
|
await store.actions.components.moveDown(component)
|
||||||
|
} else if (e.key === "c") {
|
||||||
|
e.preventDefault()
|
||||||
|
await store.actions.components.copy(component, false)
|
||||||
|
} else if (e.key === "x") {
|
||||||
|
e.preventDefault()
|
||||||
|
store.actions.components.copy(component, true)
|
||||||
|
} else if (e.key === "v") {
|
||||||
|
e.preventDefault()
|
||||||
|
await store.actions.components.paste(component, "inside")
|
||||||
|
} else if (e.key === "d") {
|
||||||
|
e.preventDefault()
|
||||||
|
await store.actions.components.copy(component)
|
||||||
|
await store.actions.components.paste(component, "below")
|
||||||
|
} else if (e.key === "Enter") {
|
||||||
|
e.preventDefault()
|
||||||
|
$goto("./new")
|
||||||
|
}
|
||||||
|
} else if (e.key === "Backspace" || e.key === "Delete") {
|
||||||
|
// Don't show confirmation for the screen itself
|
||||||
|
if (component._id === get(selectedScreen).props._id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
confirmDeleteDialog.show()
|
||||||
|
} else if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault()
|
||||||
|
await store.actions.components.selectPrevious()
|
||||||
|
} else if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault()
|
||||||
|
await store.actions.components.selectNext()
|
||||||
|
} else if (e.key === "Escape" && $isActive("./new")) {
|
||||||
|
e.preventDefault()
|
||||||
|
$goto("./")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
notifications.error("Error handling key press")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
document.addEventListener("keydown", handleKeyPress)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyPress)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Panel title="Components" showExpandIcon borderRight>
|
<Panel title="Components" showExpandIcon borderRight>
|
||||||
|
@ -116,6 +189,13 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={confirmDeleteDialog}
|
||||||
|
title="Confirm Deletion"
|
||||||
|
body={`Are you sure you want to delete "${$selectedComponent?._instanceName}"?`}
|
||||||
|
okText="Delete Component"
|
||||||
|
onOk={deleteComponent}
|
||||||
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.add-component {
|
.add-component {
|
||||||
|
|
|
@ -31,15 +31,20 @@
|
||||||
<div slot="control" class="icon">
|
<div slot="control" class="icon">
|
||||||
<Icon size="S" hoverable name="MoreSmallList" />
|
<Icon size="S" hoverable name="MoreSmallList" />
|
||||||
</div>
|
</div>
|
||||||
<MenuItem icon="Copy" on:click={() => storeComponentForCopy(false)}>
|
<MenuItem
|
||||||
|
icon="Copy"
|
||||||
|
keyBind="Ctrl+C"
|
||||||
|
on:click={() => storeComponentForCopy(false)}
|
||||||
|
>
|
||||||
Copy
|
Copy
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="ShowOneLayer"
|
icon="LayersSendToBack"
|
||||||
|
keyBind="Ctrl+V"
|
||||||
on:click={() => pasteComponent("inside")}
|
on:click={() => pasteComponent("inside")}
|
||||||
disabled={noPaste}
|
disabled={noPaste}
|
||||||
>
|
>
|
||||||
Paste inside
|
Paste
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</ActionMenu>
|
</ActionMenu>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,20 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { notifications, Slider, Icon } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
|
||||||
const ThemeOptions = [
|
const onChangeTheme = async theme => {
|
||||||
"spectrum--darkest",
|
|
||||||
"spectrum--dark",
|
|
||||||
"spectrum--light",
|
|
||||||
"spectrum--lightest",
|
|
||||||
]
|
|
||||||
|
|
||||||
$: themeIndex = ThemeOptions.indexOf($store.theme) ?? 2
|
|
||||||
|
|
||||||
const onChangeTheme = async e => {
|
|
||||||
try {
|
try {
|
||||||
const theme = ThemeOptions[e.detail] ?? ThemeOptions[2]
|
await store.actions.theme.save(`spectrum--${theme}`)
|
||||||
await store.actions.theme.save(theme)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error updating theme")
|
notifications.error("Error updating theme")
|
||||||
}
|
}
|
||||||
|
@ -22,26 +13,52 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Icon name="Moon" />
|
{#each Constants.Themes as theme}
|
||||||
<Slider
|
<div
|
||||||
min={0}
|
class="theme"
|
||||||
max={3}
|
class:selected={`spectrum--${theme.class}` === $store.theme}
|
||||||
step={1}
|
on:click={() => onChangeTheme(theme.class)}
|
||||||
value={themeIndex}
|
>
|
||||||
on:change={onChangeTheme}
|
<div
|
||||||
/>
|
style="background: {theme.preview}"
|
||||||
<Icon name="Light" />
|
class="color spectrum--{theme.class}"
|
||||||
|
/>
|
||||||
|
{theme.name}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
.color {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50px;
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.theme {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: var(--spacing-s) var(--spacing-m);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-xl);
|
||||||
|
transition: background 130ms ease-out;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
}
|
}
|
||||||
div :global(.spectrum-Form-item) {
|
.theme:hover {
|
||||||
flex: 1 1 auto;
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.theme.selected,
|
||||||
|
.theme:hover {
|
||||||
|
background: var(--spectrum-global-color-gray-50);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { Slider, Button } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let customTheme
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const options = ["0", "4px", "8px", "16px"]
|
||||||
|
|
||||||
|
$: index = options.indexOf(customTheme.buttonBorderRadius) ?? 2
|
||||||
|
|
||||||
|
const onChange = async e => {
|
||||||
|
dispatch("change", options[e.detail])
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<Slider min={0} max={3} step={1} value={index} on:change={onChange} />
|
||||||
|
<div class="button" style="--radius: {customTheme.buttonBorderRadius};">
|
||||||
|
<Button primary newStyles>Button</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.container :global(.spectrum-Form-item) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
.button :global(.spectrum-Button) {
|
||||||
|
border-radius: var(--radius) !important;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,35 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import Panel from "components/design/Panel.svelte"
|
import Panel from "components/design/Panel.svelte"
|
||||||
import {
|
import { Layout, Label, ColorPicker, notifications } from "@budibase/bbui"
|
||||||
Layout,
|
|
||||||
Label,
|
|
||||||
ColorPicker,
|
|
||||||
Button,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { DefaultAppTheme } from "constants"
|
import { DefaultAppTheme } from "constants"
|
||||||
import AppThemeSelect from "./AppThemeSelect.svelte"
|
import AppThemeSelect from "./AppThemeSelect.svelte"
|
||||||
|
import ButtonRoundnessSelect from "./ButtonRoundnessSelect.svelte"
|
||||||
const ButtonBorderRadiusOptions = [
|
|
||||||
{
|
|
||||||
label: "Square",
|
|
||||||
value: "0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Soft edge",
|
|
||||||
value: "4px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Curved",
|
|
||||||
value: "8px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Round",
|
|
||||||
value: "16px",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
$: customTheme = $store.customTheme || {}
|
$: customTheme = $store.customTheme || {}
|
||||||
|
|
||||||
|
@ -52,22 +28,11 @@
|
||||||
<AppThemeSelect />
|
<AppThemeSelect />
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Label>Buttons</Label>
|
<Label>Button roundness</Label>
|
||||||
<div class="buttons">
|
<ButtonRoundnessSelect
|
||||||
{#each ButtonBorderRadiusOptions as option}
|
{customTheme}
|
||||||
<div
|
on:change={e => update("buttonBorderRadius", e.detail)}
|
||||||
class:active={customTheme.buttonBorderRadius === option.value}
|
/>
|
||||||
style={`--radius: ${option.value}`}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
secondary
|
|
||||||
on:click={() => update("buttonBorderRadius", option.value)}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Label>Accent color</Label>
|
<Label>Accent color</Label>
|
||||||
|
@ -88,29 +53,3 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<style>
|
|
||||||
.buttons {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 100px 100px;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
}
|
|
||||||
.buttons > div {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
.buttons > div :global(.spectrum-Button) {
|
|
||||||
border-radius: var(--radius) !important;
|
|
||||||
border-width: 1px;
|
|
||||||
border-color: var(--spectrum-global-color-gray-400);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.buttons > div:hover :global(.spectrum-Button) {
|
|
||||||
background: var(--spectrum-global-color-gray-700);
|
|
||||||
border-color: var(--spectrum-global-color-gray-700);
|
|
||||||
}
|
|
||||||
.buttons > div.active :global(.spectrum-Button) {
|
|
||||||
background: var(--spectrum-global-color-gray-200);
|
|
||||||
color: var(--spectrum-global-color-gray-800);
|
|
||||||
border-color: var(--spectrum-global-color-gray-600);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -94,7 +94,7 @@
|
||||||
id="spectrum-root"
|
id="spectrum-root"
|
||||||
lang="en"
|
lang="en"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
class="spectrum spectrum--medium spectrum--darkest {$themeStore.theme}"
|
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
|
||||||
>
|
>
|
||||||
<DeviceBindingsProvider>
|
<DeviceBindingsProvider>
|
||||||
<UserBindingsProvider>
|
<UserBindingsProvider>
|
||||||
|
|
|
@ -17,10 +17,16 @@
|
||||||
--spectrum-semantic-cta-color-background-default: var(--primaryColor);
|
--spectrum-semantic-cta-color-background-default: var(--primaryColor);
|
||||||
--spectrum-semantic-cta-color-background-hover: var(--primaryColorHover);
|
--spectrum-semantic-cta-color-background-hover: var(--primaryColorHover);
|
||||||
--spectrum-semantic-cta-color-background-down: var(--primaryColorHover);
|
--spectrum-semantic-cta-color-background-down: var(--primaryColorHover);
|
||||||
--spectrum-button-primary-s-border-radius: var(--buttonBorderRadius);
|
--spectrum-button-primary-s-border-radius: calc(
|
||||||
|
var(--buttonBorderRadius) * 0.9
|
||||||
|
);
|
||||||
--spectrum-button-primary-m-border-radius: var(--buttonBorderRadius);
|
--spectrum-button-primary-m-border-radius: var(--buttonBorderRadius);
|
||||||
--spectrum-button-primary-l-border-radius: var(--buttonBorderRadius);
|
--spectrum-button-primary-l-border-radius: calc(
|
||||||
--spectrum-button-primary-xl-border-radius: var(--buttonBorderRadius);
|
var(--buttonBorderRadius) * 1.25
|
||||||
|
);
|
||||||
|
--spectrum-button-primary-xl-border-radius: calc(
|
||||||
|
var(--buttonBorderRadius) * 1.5
|
||||||
|
);
|
||||||
|
|
||||||
/* Loading spinners */
|
/* Loading spinners */
|
||||||
--spectrum-progresscircle-medium-track-fill-color: var(--primaryColor);
|
--spectrum-progresscircle-medium-track-fill-color: var(--primaryColor);
|
||||||
|
|
|
@ -5,6 +5,8 @@ import "@spectrum-css/vars/dist/spectrum-darkest.css"
|
||||||
import "@spectrum-css/vars/dist/spectrum-dark.css"
|
import "@spectrum-css/vars/dist/spectrum-dark.css"
|
||||||
import "@spectrum-css/vars/dist/spectrum-light.css"
|
import "@spectrum-css/vars/dist/spectrum-light.css"
|
||||||
import "@spectrum-css/vars/dist/spectrum-lightest.css"
|
import "@spectrum-css/vars/dist/spectrum-lightest.css"
|
||||||
|
import "@budibase/frontend-core/src/themes/nord.css"
|
||||||
|
import "@budibase/frontend-core/src/themes/midnight.css"
|
||||||
import "@spectrum-css/page/dist/index-vars.css"
|
import "@spectrum-css/page/dist/index-vars.css"
|
||||||
|
|
||||||
// Non user-facing components
|
// Non user-facing components
|
||||||
|
|
|
@ -16,20 +16,20 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
const onKeyDown = e => {
|
const onKeyDown = e => {
|
||||||
if (e.key === "Delete" || e.key === "Backspace") {
|
|
||||||
deleteSelectedComponent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteSelectedComponent = () => {
|
|
||||||
const state = get(builderStore)
|
const state = get(builderStore)
|
||||||
if (!state.inBuilder || !state.selectedComponentId || state.editMode) {
|
if (!state.inBuilder || state.editMode) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const activeTag = document.activeElement?.tagName.toLowerCase()
|
const activeTag = document.activeElement?.tagName.toLowerCase()
|
||||||
if (["input", "textarea"].indexOf(activeTag) !== -1) {
|
if (["input", "textarea"].indexOf(activeTag) !== -1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
builderStore.actions.deleteComponent(state.selectedComponentId)
|
|
||||||
|
// Need to manually block certain keys from propagating to the browser
|
||||||
|
if (e.ctrlKey && e.key === "d") {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
builderStore.actions.keyDown(e.key, e.ctrlKey)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -40,8 +40,8 @@ const createBuilderStore = () => {
|
||||||
updateProp: (prop, value) => {
|
updateProp: (prop, value) => {
|
||||||
dispatchEvent("update-prop", { prop, value })
|
dispatchEvent("update-prop", { prop, value })
|
||||||
},
|
},
|
||||||
deleteComponent: id => {
|
keyDown: (key, ctrlKey) => {
|
||||||
dispatchEvent("delete-component", { id })
|
dispatchEvent("key-down", { key, ctrlKey })
|
||||||
},
|
},
|
||||||
duplicateComponent: id => {
|
duplicateComponent: id => {
|
||||||
dispatchEvent("duplicate-component", { id })
|
dispatchEvent("duplicate-component", { id })
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { derived } from "svelte/store"
|
import { derived } from "svelte/store"
|
||||||
import { appStore } from "./app"
|
import { appStore } from "./app"
|
||||||
import { builderStore } from "./builder"
|
import { builderStore } from "./builder"
|
||||||
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
|
||||||
// This is the good old acorn bug where having the word "g l o b a l" makes it
|
// This is the good old acorn bug where having the word "g l o b a l" makes it
|
||||||
// think that this is not ES6 compatible and starts throwing errors when using
|
// think that this is not ES6 compatible and starts throwing errors when using
|
||||||
|
@ -28,6 +29,13 @@ const createThemeStore = () => {
|
||||||
// Ensure theme is set
|
// Ensure theme is set
|
||||||
theme = theme || defaultTheme
|
theme = theme || defaultTheme
|
||||||
|
|
||||||
|
// Get base theme
|
||||||
|
let base =
|
||||||
|
Constants.Themes.find(x => `spectrum--${x.class}` === theme)?.base || ""
|
||||||
|
if (base) {
|
||||||
|
base = `spectrum--${base}`
|
||||||
|
}
|
||||||
|
|
||||||
// Delete and nullish keys from the custom theme
|
// Delete and nullish keys from the custom theme
|
||||||
if (customTheme) {
|
if (customTheme) {
|
||||||
Object.entries(customTheme).forEach(([key, value]) => {
|
Object.entries(customTheme).forEach(([key, value]) => {
|
||||||
|
@ -51,6 +59,7 @@ const createThemeStore = () => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme,
|
theme,
|
||||||
|
baseTheme: base,
|
||||||
customTheme,
|
customTheme,
|
||||||
customThemeCss,
|
customThemeCss,
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,5 +12,7 @@
|
||||||
--spectrum-global-color-gray-700: hsl(var(--hue), var(--sat), 70%);
|
--spectrum-global-color-gray-700: hsl(var(--hue), var(--sat), 70%);
|
||||||
--spectrum-global-color-gray-800: hsl(var(--hue), var(--sat), 80%);
|
--spectrum-global-color-gray-800: hsl(var(--hue), var(--sat), 80%);
|
||||||
--spectrum-global-color-gray-900: hsl(var(--hue), var(--sat), 95%);
|
--spectrum-global-color-gray-900: hsl(var(--hue), var(--sat), 95%);
|
||||||
|
|
||||||
|
--modal-background: var(--spectrum-global-color-gray-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,4 +43,7 @@
|
||||||
|
|
||||||
--spectrum-alias-highlight-hover: rgba(169, 177, 193, 0.1);
|
--spectrum-alias-highlight-hover: rgba(169, 177, 193, 0.1);
|
||||||
--spectrum-alias-highlight-active: rgba(169, 177, 193, 0.1);
|
--spectrum-alias-highlight-active: rgba(169, 177, 193, 0.1);
|
||||||
|
--spectrum-alias-background-color-hover-overlay: rgba(169, 177, 193, 0.1);
|
||||||
|
|
||||||
|
--modal-background: var(--spectrum-global-color-gray-50);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue