mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-19 08:03:16 +00:00
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:
@@ -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 for 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}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user