fix(frontend): Improve theme editor (#3525)

* fix(frontend): Improve theme editor

* feat(frontend): improve theme editor
This commit is contained in:
Faton Ramadani
2024-04-09 10:21:26 +02:00
committed by GitHub
parent 16c9535256
commit 975055b90f
8 changed files with 189 additions and 169 deletions
@@ -1,13 +1,15 @@
<script lang="ts">
import { getModifierKey } from '$lib/utils'
import { Copy, Scissors, Trash } from 'lucide-svelte'
import { Copy, Paintbrush2, Scissors, Trash } from 'lucide-svelte'
import ContextMenu from '$lib/components/ContextMenu.svelte'
import ComponentCallbacks from './component/ComponentCallbacks.svelte'
import { getContext } from 'svelte'
import type { AppEditorContext, AppViewerContext } from '../types'
import DeleteComponent from './settingsPanel/DeleteComponent.svelte'
import { findComponentSettings } from './appUtils'
import { secondaryMenuLeft } from './settingsPanel/secondaryMenu'
import StylePanel from './settingsPanel/StylePanel.svelte'
export let id: string
let componentCallbacks: ComponentCallbacks | undefined = undefined
@@ -40,6 +42,14 @@
icon: Copy,
shortcut: `${getModifierKey()}C`
},
{
label: 'Show style panel',
onClick: () => {
secondaryMenuLeft?.toggle(StylePanel, {})
},
icon: Paintbrush2,
disabled: $secondaryMenuLeft.isOpen
},
{
label: 'Delete',
@@ -19,7 +19,6 @@
shouldCapitalize={true}
resourceOnly={false}
fieldType="text"
tooltip="Eval an expression that return a list of class as string to dynamically add classes to the component. The styling can then be dynamic using the global CSS Editor."
customTitle="Dynamic class (eval)"
displayType={false}
placeholder={undefined}
@@ -1,10 +1,9 @@
<script lang="ts">
import { Copy, MoveLeft, MoveRight, Paintbrush2 } from 'lucide-svelte'
import { Code, Copy, MoveLeft, MoveRight, Paintbrush2 } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
import { fade } from 'svelte/transition'
import { slide } from 'svelte/transition'
import { addWhitespaceBeforeCapitals, copyToClipboard, sendUserToast } from '../../../../utils'
import { Button, ClearableInput } from '../../../common'
import Popover from '../../../Popover.svelte'
import type { ComponentCssProperty } from '../../types'
import { ccomponents, type TypedComponent } from '../component'
import QuickStyleMenu from './QuickStyleMenu.svelte'
@@ -15,6 +14,9 @@
import CssEval from './CssEval.svelte'
import parse from 'style-to-object'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import Popover from '$lib/components/Popover.svelte'
export let name: string
export let value: ComponentCssProperty = {}
@@ -40,125 +42,125 @@
value.style = ''
}
parse(value.style)
isQuickMenuOpen = !isQuickMenuOpen
} catch {
sendUserToast('Invalid CSS: Rich editor cannot be toggled', true)
}
}
let richEditorOpen = false
let dynamicClass: boolean = value?.evalClass !== undefined
let render = 0
</script>
{#key render}
<div class=" border-b flex justify-between items-center p-2 text-xs leading-6 font-bold">
<div class="flex flex-col gap-1 w-full items-start">
<div class="flex flex-row h-8 items-center justify-between w-full">
<div class="capitalize">
{addWhitespaceBeforeCapitals(name)}
</div>
{#if shouldDisplayLeft}
<div class="flex justify-between items-center p-2 text-xs leading-6 font-bold w-full">
<div class="capitalize">
{addWhitespaceBeforeCapitals(name)}
</div>
<div class="flex flex-row items-center gap-1">
{#if shouldDisplayLeft}
<Popover placement="bottom" notClickable disappearTimeout={0}>
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
dispatch('left')
}}
>
<div class="flex flex-row gap-2 text-2xs items-center">
<MoveLeft size={14} />
Copy for this component
</div>
</Button>
{/if}
{#if shouldDisplayRight}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
dispatch('right')
}}
>
<div class="flex flex-row gap-2 text-2xs items-center">
Copy for every {componentType ? ccomponents[componentType].name : 'component'}
<MoveRight size={14} />
</div>
</Button>
{/if}
</div>
{#if wmClass}
<Badge small>
<div class="flex flex-row gap-1 items-center">
{wmClass}
<Button
color="light"
size="xs2"
on:click={() => {
copyToClipboard(wmClass)
}}
>
<Copy size={14} />
</Button>
</div>
</Badge>
iconOnly
startIcon={{ icon: MoveLeft }}
on:click={() => dispatch('left')}
/>
<svelte:fragment slot="text">{'Copy for this component'}</svelte:fragment>
</Popover>
{/if}
{#if shouldDisplayRight}
<Popover placement="bottom" notClickable disappearTimeout={0}>
<Button
color="light"
size="xs2"
iconOnly
startIcon={{ icon: MoveRight }}
on:click={() => dispatch('right')}
/>
<svelte:fragment slot="text">
Copy for every {componentType ? ccomponents[componentType].name : 'component'}
</svelte:fragment>
</Popover>
{/if}
<Popover placement="bottom" notClickable disappearTimeout={0}>
<Button
color="light"
size="xs2"
iconOnly
startIcon={{ icon: Copy }}
on:click={() => copyToClipboard(wmClass)}
/>
<svelte:fragment slot="text">
Copy {wmClass}
</svelte:fragment>
</Popover>
</div>
</div>
{#if value}
<div class="p-2 flex flex-col">
<div class="p-2 flex flex-col gap-2">
{#if tooltip}
<div class="text-tertiary text-2xs py-2">{tooltip}</div>
{/if}
{#if value.style !== undefined || forceStyle}
<div class="pb-2">
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="block w-full">
<div class="flex flex-row justify-between items-center w-full h-8">
<div class="block w-full">
<div class="flex flex-row justify-between items-center w-full h-8 mb-1">
<div class="text-xs font-medium text-tertiary"> Plain CSS </div>
{#if overriden}
<Badge color="red" small>Overriden by local</Badge>
{:else if overridding}
<Badge color="blue" small>Overriding global</Badge>
{/if}
</div>
<div class="flex gap-1">
<div class="relative grow">
<ClearableInput
bind:value={value.style}
type="textarea"
wrapperClass="h-full min-h-[72px]"
inputClass="h-full !text-xs !rounded-none !p-2"
/>
</div>
<div class="flex flex-col gap-1">
<div class="flex flex-row gap-1">
{#if overriden}
<Badge color="red" small>Overriden by local</Badge>
{:else if overridding}
<Badge color="blue" small>Overriding global</Badge>
{/if}
{#if quickStyleProperties?.length}
<Popover placement="bottom" notClickable disappearTimeout={0}>
<Button
variant="border"
color="light"
size="xs"
btnClasses="!p-1 !w-[34px] !h-[34px] {isQuickMenuOpen
? '!bg-gray-200/60 hover:!bg-gray-200 focus:!bg-gray-200'
: ''}"
aria-label="{isQuickMenuOpen ? 'Close' : 'Open'} styling menu"
on:click={toggleQuickMenu}
>
<Paintbrush2 size={18} />
</Button>
<svelte:fragment slot="text">
{isQuickMenuOpen ? 'Close' : 'Open'} styling menu
</svelte:fragment>
</Popover>
<ToggleButtonGroup
bind:selected={richEditorOpen}
on:selected={() => {
if (richEditorOpen !== isQuickMenuOpen) {
toggleQuickMenu()
richEditorOpen = isQuickMenuOpen
}
}}
>
<ToggleButton
small
light
value={false}
icon={Code}
tooltip="Edit the CSS directly"
/>
<ToggleButton
small
light
value={true}
icon={Paintbrush2}
tooltip="Open the rich editor to style the component with a visual interface"
/>
</ToggleButtonGroup>
{/if}
</div>
</div>
</label>
<ClearableInput
bind:value={value.style}
type="textarea"
disabled={isQuickMenuOpen}
wrapperClass="h-full min-h-[72px]"
inputClass="h-full !text-xs !rounded-none !p-2 !shadow-none !border-gray-200 dark:!border-gray-600 "
/>
</div>
{#if quickStyleProperties?.length && isQuickMenuOpen}
<div transition:fade|local={{ duration: 200 }} class="w-full pt-1">
<div class="text-xs mb-1 font-medium">Rich editor</div>
<div transition:slide|local={{ duration: 200 }} class="w-full">
<QuickStyleMenu
bind:value={value.style}
properties={quickStyleProperties}
@@ -188,7 +190,7 @@
{#if value.class !== undefined || forceClass}
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="block">
<div class="text-xs font-medium text-tertiary">
<div class="text-xs font-medium text-tertiary mb-1">
Tailwind classes
<Tooltip light documentationLink="https://tailwindcss.com/">
Use any tailwind classes to style your component
@@ -196,7 +198,7 @@
</div>
<div class="relative">
<SimpleEditor
class="h-24"
class="h-24 border !rounded-none"
lang="tailwindcss"
bind:code={value.class}
fixedOverflowWidgets={true}
@@ -222,27 +224,31 @@
{/if}
</label>
{/if}
<Toggle
options={{
right: 'Use dynamic class'
}}
size="xs"
bind:checked={dynamicClass}
on:change={(e) => {
if (e.detail && !value.evalClass) {
value.evalClass = {
type: 'evalv2',
expr: '',
connections: [],
fieldType: 'text'
<div class="flex flex-row justify-between items-center">
<div class="text-xs flex flex-row items-center justify-center">
Use dynamic class
<Tooltip light>
Eval an expression that return a list of class as string to dynamically add classes to
the component. The styling can then be dynamic using the global CSS Editor.
</Tooltip>
</div>
<Toggle
size="xs"
bind:checked={dynamicClass}
on:change={(e) => {
if (e.detail && !value.evalClass) {
value.evalClass = {
type: 'evalv2',
expr: '',
connections: [],
fieldType: 'text'
}
} else {
value.evalClass = undefined
}
} else {
value.evalClass = undefined
}
}}
/>
}}
/>
</div>
{#if value?.evalClass && dynamicClass}
<CssEval key={name} bind:evalClass={value.evalClass} />
{/if}
@@ -23,6 +23,7 @@
const { app } = getContext<AppViewerContext>('AppViewerContext')
const styleStore = createStyleStore(properties)
setContext(STYLE_STORE_KEY, styleStore)
let multiValues: Record<number, string[]> = initiateMultiValues()
let mounted = false
let isOpen: Record<string, boolean> = {}
@@ -75,7 +76,14 @@
function setTopColors() {
const styles = collectStyles()
const parsedStyles = styles.map((style) => parse(style) || {})
const parsedStyles = styles.map((style) => {
try {
return parse(style) || {}
} catch {
return {}
}
})
const usedColors: Record<string, number> = {}
parsedStyles.forEach((style) => {
Object.values(style).reduce((colors, v) => {
@@ -174,7 +182,7 @@
})
</script>
<div class="pb-2 pt-1">
<div class="flex flex-col !divide-y">
{#each properties as property}
{#each Object.keys(property) as group (group)}
{@const prefix = `${componentType}_${componentProperty}_${group}`}
@@ -182,13 +190,13 @@
bind:isOpen={isOpen[prefix]}
title={group}
{prefix}
openByDefault={true}
wrapperClasses="!px-0 !pt-0"
toggleClasses="border-b !rounded-b-none !py-0
openByDefault={false}
wrapperClasses="!px-0 !py-0 "
toggleClasses=" !rounded-b-none !py-0
{isOpen[prefix] ? '!bg-surface-secondary hover:!bg-surface-hover' : ''}"
>
<svelte:fragment slot="title">
<span class="font-semibold text-tertiary capitalize">
<span class="font-normal text-xs">
{group}
</span>
</svelte:fragment>
@@ -200,7 +208,7 @@
} = styleStore.getProp(p)}
{#if prop !== undefined && index !== undefined}
<div class="pb-2 pt-1">
<div class="text-sm font-medium text-tertiary pb-0.5">
<div class="text-xs font-semibold text-tertiary pb-0.5">
{formatKebabCase(prop.key)}
</div>
<div class="flex items-center gap-1">
@@ -611,7 +611,8 @@ export const quickStyleProperties: Record<
aggridcomponent: {},
aggridcomponentee: {},
buttoncomponent: {
button: buttonDefaultProps
button: buttonDefaultProps,
container: containerDefaultProps
},
downloadcomponent: {
button: buttonDefaultProps
@@ -167,9 +167,9 @@
componentSettings.item.data.componentInput = appInput
}
}
function isMac() {
return navigator.platform.toUpperCase().indexOf('MAC') >= 0;
return navigator.platform.toUpperCase().indexOf('MAC') >= 0
}
</script>
@@ -115,10 +115,10 @@
{#if component}
{#key component?.id}
<div class="px-2 flex items-center gap-2 flex-row justify-between">
{#if !cssEditorOpen}
<div class="px-2 flex gap-1 flex-col w-full pb-4">
{#if !$cssEditorOpen}
<Button
color="blue"
color="light"
size="xs2"
variant="border"
on:click={() => {
@@ -126,7 +126,7 @@
}}
>
<div class="flex flex-row gap-1 text-xs items-center">
Open CSS editor{$enterpriseLicense === undefined ? ' (EE only)' : ''}
Open theme editor{$enterpriseLicense === undefined ? ' (EE only)' : ''}
<Tooltip light>
You can also use the App CSS Editor to customise the CSS of all components.
</Tooltip>
@@ -136,19 +136,18 @@
<div />
{/if}
<div class="flex flex-row gap-2 items-center justify-between">
{#if $enterpriseLicense !== undefined}
<Button
color="dark"
size="xs2"
on:click={() => {
migrationModal?.open()
}}
>
Convert to global CSS
</Button>
{/if}
</div>
{#if $enterpriseLicense !== undefined}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
migrationModal?.open()
}}
>
Convert to global CSS
</Button>
{/if}
</div>
<Tabs bind:selected={tab}>
@@ -178,28 +177,30 @@
<svelte:fragment slot="content">
<TabContent value="local">
{#if component && component.customCss !== undefined}
{#each Object.keys(ccomponents[component.type].customCss ?? {}) as name}
<div class="w-full">
<CssProperty
quickStyleProperties={quickStyleProperties?.[component.type]?.[name]}
forceStyle={ccomponents[component.type].customCss[name].style !== undefined}
forceClass={ccomponents[component.type].customCss[name].class !== undefined}
tooltip={ccomponents[component.type].customCss[name].tooltip}
{name}
wmClass={getSelector(name)}
componentType={component.type}
bind:value={component.customCss[name]}
on:change={() => app.set($app)}
shouldDisplayRight={hasStyleValue(component.customCss[name])}
on:right={() => {
copyLocalToGlobal(name, component?.customCss?.[name])
tab = 'global'
}}
overridding={hasStyleValue($app.css?.[component.type]?.[name]) &&
hasStyleValue(component.customCss[name])}
/>
</div>
{/each}
<div class="flex-col flex gap-4 divide-y">
{#each Object.keys(ccomponents[component.type].customCss ?? {}) as name}
<div class="w-full">
<CssProperty
quickStyleProperties={quickStyleProperties?.[component.type]?.[name]}
forceStyle={ccomponents[component.type].customCss[name].style !== undefined}
forceClass={ccomponents[component.type].customCss[name].class !== undefined}
tooltip={ccomponents[component.type].customCss[name].tooltip}
{name}
wmClass={getSelector(name)}
componentType={component.type}
bind:value={component.customCss[name]}
on:change={() => app.set($app)}
shouldDisplayRight={hasStyleValue(component.customCss[name])}
on:right={() => {
copyLocalToGlobal(name, component?.customCss?.[name])
tab = 'global'
}}
overridding={hasStyleValue($app.css?.[component.type]?.[name]) &&
hasStyleValue(component.customCss[name])}
/>
</div>
{/each}
</div>
{/if}
</TabContent>
<TabContent value="global">
@@ -1,6 +1,5 @@
<script lang="ts">
import { fly } from 'svelte/transition'
import { Badge } from '../../../../common'
import { secondaryMenuLeft, secondaryMenuRight } from './'
import { getContext } from 'svelte'
import type { AppViewerContext } from '../../../types'
@@ -34,11 +33,7 @@
class="flex justify-between {right ? '' : 'flex-row-reverse'} items-center gap-1 px-3 py-2"
>
<CloseButton on:close={() => secondaryMenu?.close()} />
{#if $selectedComponent}
<Badge color="blue">{$selectedComponent}</Badge>
{:else}
<div />
{/if}
<div class="text-xs font-bold"> Style Panel</div>
</div>
<div class="relative h-full overflow-y-auto">
{#if typeof $secondaryMenu.component === 'string'}