feat(frontend): Add more app component CSS customisation (#1218)

* fix(frontend): Update app style editor

* fix(frontend): Update component custom css typings

* fix(frontend): Generalise custom CSS inputs

* feat(frontend): Add display CSS customization
This commit is contained in:
Ádám Kovács
2023-02-23 11:42:32 +01:00
committed by GitHub
parent 18ff5c7cef
commit 6044e3b6ef
13 changed files with 236 additions and 172 deletions
@@ -1,25 +1,36 @@
<script lang="ts">
import DisplayResult from '$lib/components/DisplayResult.svelte'
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
import type { AppInput } from '../inputType'
import { IS_APP_PUBLIC_CONTEXT_KEY } from '../types'
import { IS_APP_PUBLIC_CONTEXT_KEY, type AppEditorContext, type ComponentCustomCSS } from '../types'
import RunnableWrapper from './helpers/RunnableWrapper.svelte'
export let id: string
export let componentInput: AppInput | undefined
export let initializing: boolean | undefined = undefined
export let customCss: ComponentCustomCSS<'header' | 'container'> | undefined = undefined
const requireHtmlApproval = getContext<boolean | undefined>(IS_APP_PUBLIC_CONTEXT_KEY)
const { app } = getContext<AppEditorContext>('AppEditorContext')
let result: any = undefined
export const staticOutputs: string[] = ['result', 'loading']
</script>
<RunnableWrapper flexWrap bind:componentInput {id} bind:initializing bind:result>
<div class="w-full border-b px-2 text-xs p-1 font-semibold bg-gray-500 text-white rounded-t-sm">
<div class={twMerge(
'w-full border-b px-2 text-xs p-1 font-semibold bg-gray-500 text-white rounded-t-sm',
$app.css?.['displaycomponent']?.['header']?.class,
customCss?.header?.class
)}>
Results
</div>
<div class="p-2">
<div class={twMerge(
'p-2',
$app.css?.['displaycomponent']?.['container']?.class,
customCss?.container?.class
)}>
<DisplayResult {result} {requireHtmlApproval} />
</div>
</RunnableWrapper>
@@ -1,9 +1,9 @@
<script lang="ts">
import { Button, type ButtonType } from '$lib/components/common'
import { getContext, onMount } from 'svelte'
import { getContext } from 'svelte'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppEditorContext } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
import type RunnableComponent from '../helpers/RunnableComponent.svelte'
@@ -20,7 +20,7 @@
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export let noWFull = false
export let preclickAction: (() => Promise<void>) | undefined = undefined
export let customCss: Record<'button', { class: string; style: string }> | undefined = undefined
export let customCss: ComponentCustomCSS<'button'> | undefined = undefined
export const staticOutputs: string[] = ['loading', 'result']
@@ -119,10 +119,10 @@
<Button
btnClasses={twMerge(
$app.css?.['buttoncomponent']?.['button']?.class,
customCss?.button?.class,
customCss?.button.class,
fillContainer ? 'w-full h-full' : ''
)}
style={[$app.css?.['buttoncomponent']?.['button']?.style, customCss?.button?.style].join(';')}
style={[$app.css?.['buttoncomponent']?.['button']?.style, customCss?.button.style].join(';')}
{disabled}
on:pointerdown={(e) => {
e?.stopPropagation()
@@ -8,7 +8,7 @@
import InputValue from '../helpers/InputValue.svelte'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import { twMerge } from 'tailwind-merge'
import type { AppEditorContext } from '../../types'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { getContext } from 'svelte'
export let id: string
@@ -17,7 +17,7 @@
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export let configuration: Record<string, AppInput>
export let initializing: boolean | undefined = undefined
export let customCss: Record<'text', { class: string; style: string }> | undefined = undefined
export let customCss: ComponentCustomCSS<'text'> | undefined = undefined
export const staticOutputs: string[] = ['result', 'loading']
@@ -194,21 +194,21 @@
</Pane>
<Pane size={21} minSize={5} maxSize={33}>
<div class="relative flex flex-col h-full">
<Tabs bind:selected={selectedTab}>
<Tab value="insert" size="xs">
<div class="m-1 flex flex-row gap-2">
<Tabs bind:selected={selectedTab} class="!border-b-2 !border-gray-200">
<Tab value="insert" size="xs" class="grow">
<div class="m-1 center-center gap-2">
<Icon data={faPlus} />
<span>Insert</span>
</div>
</Tab>
<Tab value="settings" size="xs">
<div class="m-1 flex flex-row gap-2">
<Tab value="settings" size="xs" class="grow">
<div class="m-1 center-center gap-2">
<Icon data={faSliders} />
<span>Settings</span>
</div>
</Tab>
<Tab value="css" size="xs">
<div class="m-1 flex flex-row gap-2">
<Tab value="css" size="xs" class="grow">
<div class="m-1 center-center gap-2">
<Icon data={faCode} />
<span>CSS</span>
</div>
@@ -218,7 +218,9 @@
{#if $selectedComponent !== undefined}
<SettingsPanel />
{:else}
<div class="p-2 min-w-[150px] text-sm">No component selected.</div>
<div class="min-w-[150px] text-sm text-gray-500 text-center py-8 px-2">
Select a component to see the settings&nbsp;for&nbsp;it
</div>
{/if}
</TabContent>
<TabContent value="insert">
@@ -133,7 +133,6 @@
*/
dims: AppComponentDimensions
data: AppComponent
cssIds?: string[]
}
export const components: Record<AppComponent['type'], AppComponentConfig> = {
@@ -150,7 +149,10 @@
value: { foo: 42 }
},
configuration: {},
customCss: {},
customCss: {
header: { class: '', style: '' },
container: { class: '', style: '' },
} as const,
card: false
}
},
@@ -178,7 +180,6 @@
name: 'Text',
icon: Type,
dims: '1:1-3:1',
cssIds: ['text'],
data: {
softWrap: false,
horizontalAlignment: 'left',
@@ -207,7 +208,7 @@
},
customCss: {
text: { class: '', style: '' }
},
} as const,
card: false
}
},
@@ -215,7 +216,6 @@
name: 'Button',
icon: Inspect,
dims: '1:1-2:1',
cssIds: ['button'],
data: {
...defaultAlignement,
softWrap: true,
@@ -286,7 +286,7 @@
},
customCss: {
button: { style: '', class: '' }
},
} as const,
card: false
}
},
@@ -1,12 +1,8 @@
<script lang="ts">
import { slide } from 'svelte/transition'
import Icon from 'svelte-awesome'
import type { AppEditorContext, GridItem } from '../../types'
import gridHelp from '@windmill-labs/svelte-grid/src/utils/helper'
import { getContext, onMount } from 'svelte'
import { getNextId } from '$lib/components/flows/flowStateUtils'
import { faAngleDown } from '@fortawesome/free-solid-svg-icons'
import { isOpenStore } from './store'
import { gridColumns } from '../../gridUtils'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
@@ -16,7 +12,9 @@
getRecommendedDimensionsByComponent,
type AppComponent
} from '../Component.svelte'
import ListItem from './ListItem.svelte'
const TITLE_PREFIX = 'Component.' as const
const { app, selectedComponent, focusedGrid } = getContext<AppEditorContext>('AppEditorContext')
// The grid is needed to find a space for the new component
@@ -98,45 +96,32 @@
}
onMount(() => {
isOpenStore.addItems(componentSets.map((set) => ({ [set.title]: true })))
isOpenStore.addItems(componentSets.map((set) => ({ [TITLE_PREFIX + set.title]: true })))
})
</script>
{#each componentSets as { title, components }, index (index)}
{@const isOpen = $isOpenStore[title]}
<section class="mt-1 mb-2 px-2">
<button
on:click|preventDefault={() => isOpenStore.toggle(title)}
class="w-full flex justify-between items-center text-gray-700 px-2 py-1
rounded-sm duration-200 hover:bg-gray-100"
>
<h1 class="text-sm font-semibold text-left">{title}</h1>
<Icon data={faAngleDown} class="rotate-0 duration-300 {isOpen ? '!rotate-180' : ''}" />
</button>
{#if isOpen}
<div transition:slide|local={{ duration: 300 }}>
{#if components.length}
<div class="flex flex-wrap gap-2 py-2">
{#each components as item}
<button
on:click={() => addComponent(item)}
title={componentsRecord[item].name}
class="border w-24 shadow-sm h-16 p-2 flex flex-col gap-2 items-center
justify-center bg-white rounded-md hover:bg-gray-100 duration-200"
>
<svelte:component this={componentsRecord[item].icon} />
<div class="text-xs w-full text-center ellipsize">
{componentsRecord[item].name}
</div>
</button>
{/each}
</div>
{:else}
<div class="text-xs text-gray-500 py-1 px-2">
There are no components in this group yet
</div>
{/if}
<ListItem {title} prefix={TITLE_PREFIX}>
{#if components.length}
<div class="flex flex-wrap gap-2 py-2">
{#each components as item}
<button
on:click={() => addComponent(item)}
title={componentsRecord[item].name}
class="border w-24 shadow-sm h-16 p-2 flex flex-col gap-2 items-center
justify-center bg-white rounded-md hover:bg-gray-100 duration-200"
>
<svelte:component this={componentsRecord[item].icon} />
<div class="text-xs w-full text-center ellipsize">
{componentsRecord[item].name}
</div>
</button>
{/each}
</div>
{:else}
<div class="text-xs text-gray-500 py-1 px-2">
There are no components in this group yet
</div>
{/if}
</section>
</ListItem>
{/each}
@@ -0,0 +1,34 @@
<script lang="ts">
import type { ComponentCssProperty } from "../../types"
export let name: string
export let value: ComponentCssProperty | undefined
</script>
<div class="text-sm font-semibold text-gray-500 capitalize pt-2">
{name}
</div>
{#if value}
<div class="border-l border-gray-400/80 py-1 pl-3.5 mt-1 ml-0.5">
<label class="block pb-2">
<div class="text-xs font-medium pb-0.5">
Style
</div>
<input
type="text"
bind:value={value.style}
on:focus
/>
</label>
<label class="block">
<div class="text-xs font-medium pb-0.5">
Tailwind classes
</div>
<input
type="text"
bind:value={value.class}
on:focus
/>
</label>
</div>
{/if}
@@ -1,17 +1,23 @@
<script lang="ts">
import { onMount, getContext } from 'svelte'
import { LayoutDashboardIcon, MousePointer2, CurlyBraces } from 'lucide-svelte'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { emptyString } from '$lib/utils'
import { faAngleDown } from '@fortawesome/free-solid-svg-icons'
import { LayoutDashboardIcon } from 'lucide-svelte'
import { getContext } from 'svelte'
import Icon from 'svelte-awesome'
import { slide } from 'svelte/transition'
import { Tab, TabContent, Tabs } from '../../../common'
import type { AppEditorContext } from '../../types'
import { components, type AppComponent } from '../Component.svelte'
import { isOpenStoreCss } from './store'
import ListItem from './ListItem.svelte'
import { isOpenStore } from './store'
import CssProperty from './CssProperty.svelte'
interface CustomCSSEntry {
type: 'app' | AppComponent['type'];
name: string;
icon: any;
ids: string[];
}
const TITLE_PREFIX = 'Css.' as const
const { app } = getContext<AppEditorContext>('AppEditorContext')
let rawCode = ''
@@ -38,28 +44,23 @@
}
}
const entries: { type: 'app' | AppComponent['type']; name: string; icon: any; ids: string[] }[] =
[
{
type: 'app' as 'app' | AppComponent['type'],
name: 'App',
icon: LayoutDashboardIcon,
ids: ['viewer', 'grid', 'component']
}
].concat(
Object.entries(components).map((c) => ({
type: c[1].data.type as 'app' | AppComponent['type'],
name: c[1].name,
icon: c[1].icon,
ids: c[1].cssIds ?? []
}))
)
const entries: CustomCSSEntry[] = [
{
type: 'app',
name: 'App',
icon: LayoutDashboardIcon,
ids: ['viewer', 'grid', 'component']
},
...Object.values(components).map(({ name, icon, data: { type, customCss }}) => ({
type,
name,
icon,
ids: Object.keys(customCss ?? {}) ?? []
}))
]
let isCustom: Record<string, boolean> = Object.fromEntries(
Object.keys(entries).map((k) => [k, false])
)
if (Object.keys($isOpenStoreCss).length == 0) {
$isOpenStoreCss = Object.fromEntries(Object.keys(entries).map((k) => [k, false]))
}
let newCss = $app.css ?? {}
entries.forEach((e) => {
@@ -82,73 +83,78 @@
})
//@ts-ignore
$app.css = newCss
onMount(() => {
isOpenStore.addItems(
[{ name: 'App' }, ...Object.values(components)].map(component => {
return { [TITLE_PREFIX + component.name]: false }
})
)
})
</script>
<div class="flex flex-row-reverse px-2">
<Toggle
on:change={(e) => switchTab(e.detail)}
options={{
right: 'As JSON'
}}
/>
</div>
{#if !viewJsonSchema}
<div class="flex flex-col gap-2 p-1">
{#each entries as { type, name, icon, ids }}
{#if ids.length > 0}
<div>
<button
on:click|preventDefault={() => ($isOpenStoreCss[type] = !$isOpenStoreCss[type])}
class="w-full flex justify-between items-center px-1 py-1
rounded-sm duration-200 hover:bg-gray-100"
>
<h3 class="inline-flex gap-2 {isCustom[type] ? 'text-gray-800' : 'text-gray-500'}"
>{name} <svelte:component this={icon} />
</h3>
<Icon
data={faAngleDown}
class="rotate-0 duration-300 {$isOpenStoreCss[type] ? '!rotate-180' : ''}"
/>
</button>
{#if $isOpenStoreCss[type]}
<div transition:slide|local={{ duration: 300 }} class="flex flex-col px-2 border">
<Tabs
selected="ui"
on:selected={(e) => switchTab(e.detail === 'json')}
class="relative"
>
<Tab value="ui" size="xs" class="grow">
<div class="m-1 center-center">
<MousePointer2 size={16} />
<span class="pl-1">UI</span>
</div>
</Tab>
<Tab value="json" size="xs" class="grow">
<div class="m-1 center-center">
<CurlyBraces size={16} />
<span class="pl-1">JSON</span>
</div>
</Tab>
<div slot="content" class="h-full overflow-y-auto">
<TabContent value="ui">
{#each entries as { type, name, icon, ids }}
{#if ids.length > 0}
<ListItem title={name} prefix={TITLE_PREFIX}>
<div slot="title" class="flex items-center">
<svelte:component this={icon} size={18} />
<span class="ml-1">
{name}
</span>
</div>
<div class="pb-2">
{#each ids as id}
<div class="mb-2">
<div class="mt-1 font-semibold">{id}</div>
{#if $app?.css?.[type]?.[id]}
<span class="text-xs">Style</span>
<input
type="text"
<div class="mb-3">
{#if $app?.css?.[type][id]}
<CssProperty
name={id}
bind:value={$app.css[type][id]}
on:focus={() => (isCustom[type] = true)}
bind:value={$app.css[type][id].style}
/>
<span class="text-xs">Tailwind classes</span>
<input
type="text"
on:focus={() => (isCustom[type] = true)}
bind:value={$app.css[type][id].class}
/>
{/if}
</div>
{/each}
</div>
{/if}
</div>
</ListItem>
{/if}
{/each}
</TabContent>
<TabContent value="json">
{#if !emptyString(jsonError)}
<span class="text-red-400 text-xs mb-1 flex flex-row-reverse">
{jsonError}
</span>
{:else}
<div class="py-2" />
{/if}
{/each}
<div class="h-full w-full py-1">
<SimpleEditor
autoHeight
class="editor"
lang="json"
bind:code={rawCode}
fixedOverflowWidgets={false}
/>
</div>
</TabContent>
</div>
{:else}
{#if !emptyString(jsonError)}<span class="text-red-400 text-xs mb-1 flex flex-row-reverse"
>{jsonError}</span
>{:else}<div class="py-2" />{/if}
<div class="h-full w-full border p-1 rounded">
<SimpleEditor
autoHeight
class="editor"
lang="json"
bind:code={rawCode}
fixedOverflowWidgets={false}
/>
</div>
{/if}
</Tabs>
@@ -0,0 +1,31 @@
<script lang="ts">
import { slide } from 'svelte/transition'
import { ChevronDown } from 'lucide-svelte'
import { isOpenStore } from './store'
export let title: string
export let prefix: string = ''
$: storeTitle = prefix + title
$: isOpen = $isOpenStore[storeTitle]
</script>
<section class="mt-1 mb-2 px-1">
<button
on:click|preventDefault={() => isOpenStore.toggle(storeTitle)}
class="w-full flex justify-between items-center text-gray-700 px-2 py-1
rounded-sm duration-200 hover:bg-gray-100"
>
<h1 class="text-sm font-semibold text-left">
<slot name="title">
{title}
</slot>
</h1>
<ChevronDown class="rotate-0 duration-300 {isOpen ? '!rotate-180' : ''}" />
</button>
{#if isOpen}
<div transition:slide|local={{ duration: 300 }} class="px-2">
<slot />
</div>
{/if}
</section>
@@ -19,5 +19,3 @@ export const isOpenStore = {
},
reset: () => store.set({})
}
export const isOpenStoreCss = writable<Record<string, boolean>>({})
@@ -18,6 +18,7 @@
import RunnableInputEditor from './inputEditor/RunnableInputEditor.svelte'
import TemplateEditor from '$lib/components/TemplateEditor.svelte'
import type { AppComponent } from '../Component.svelte'
import CssProperty from '../componentsPanel/CssProperty.svelte'
export let component: AppComponent | undefined
export let onDelete: (() => void) | undefined = undefined
@@ -170,14 +171,12 @@
{#if Object.keys(component.customCss ?? {}).length > 0}
<PanelSection title="Custom CSS">
{#each Object.entries(component.customCss ?? {}) as [key, value]}
<div class="mb-2">
<div class="mt-1 font-semibold">{key}</div>
<span class="text-xs">Style</span>
<input type="text" bind:value={value.style} />
<span class="text-xs">Tailwind classes</span>
<input type="text" bind:value={value.class} />
</div>
{#each Object.keys(component.customCss ?? {}) as name}
{#if component?.customCss?.[name]}
<div class="mb-2">
<CssProperty {name} bind:value={component.customCss[name]} />
</div>
{/if}
{/each}
</PanelSection>
{/if}
+10 -12
View File
@@ -2,7 +2,7 @@ import type { Schema } from '$lib/common'
import type { Preview } from '$lib/gen'
import type { FilledItem } from '@windmill-labs/svelte-grid'
import type { Writable } from 'svelte/store'
import type { AppComponent } from './editor/Component.svelte'
import type { AppComponent, components } from './editor/Component.svelte'
import type {
AppInput,
ConnectedAppInput,
@@ -29,6 +29,13 @@ export interface GeneralAppInput {
tooltip?: string
}
export type ComponentCssProperty = {
class: string
style: string
}
export type ComponentCustomCSS<T extends string = string> = Record<T, ComponentCssProperty>
export interface BaseAppComponent extends Partial<Aligned> {
id: ComponentID
componentInput: AppInput | undefined
@@ -45,13 +52,7 @@ export interface BaseAppComponent extends Partial<Aligned> {
)
>
card: boolean | undefined
customCss?: Record<
string,
{
class: string
style: string
}
>
customCss?: ComponentCustomCSS
/**
* If `true` then the wrapper will allow items to flow outside of it's borders.
*
@@ -97,10 +98,7 @@ export type App = {
inlineScript: InlineScript | undefined
fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
}>
css?: Record<
'viewer' | 'grid' | AppComponent['type'],
Record<string, { style?: string; class?: string }>
>
css?: Record<'viewer' | 'grid' | AppComponent['type'], ComponentCustomCSS>
}
export type ConnectingInput = {
@@ -41,7 +41,7 @@
$$props.class
)}
>
<slot />
<slot {selected} />
</div>
</div>
<slot name="content" />