feat(frontend): Stat card improvement (#2709)

* feat(frontend): add class+ style support + added support for custom images

* feat(frontend): add class+ style support + added support for custom images

* feat(frontend): fix styling

* feat(frontend): add quick style buttons

* feat(frontend): fix quick tailwind class

* feat(frontend): add space only if needed

* feat(frontend): fix build

* feat(frontend): fix line heights
This commit is contained in:
Faton Ramadani
2023-11-28 22:37:11 +01:00
committed by GitHub
parent 9525ab7bba
commit 89abb68f63
6 changed files with 321 additions and 176 deletions
@@ -2,13 +2,14 @@
import { getContext } from 'svelte'
import { initConfig, initOutput } from '../../editor/appUtils'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { initCss } from '../../utils'
import { getImageDataURL, initCss } from '../../utils'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
import { ArrowDown } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
import { loadIcon } from '../icon'
import Loader from '../helpers/Loader.svelte'
export let id: string
export let configuration: RichConfigurations
@@ -28,13 +29,15 @@
let iconComponent: any
$: resolvedConfig.icon && handleIcon()
$: isIcon && resolvedConfig?.media?.configuration?.icon.icon && handleIcon()
async function handleIcon() {
if (resolvedConfig.icon) {
iconComponent = await loadIcon(resolvedConfig.icon)
if (resolvedConfig?.media?.configuration?.icon.icon) {
iconComponent = await loadIcon(resolvedConfig.media.configuration.icon.icon)
}
}
$: isIcon = resolvedConfig.media?.selected == 'icon'
</script>
{#each Object.keys(components['statcomponent'].initialData.configuration) as key (key)}
@@ -57,16 +60,60 @@
{/each}
{#if render}
<div class="p-6 border rounded-md h-full flex flex-row gap-6 w-full items-center">
{#if resolvedConfig.icon && iconComponent}
<div class="bg-blue-500 rounded-md p-2">
<svelte:component this={iconComponent} size={24} color="white" />
</div>
{/if}
<div
class={twMerge(
'flex flex-row gap-4 items-center p-4 rounded-md shadow-md h-full',
css?.container?.class,
'wm-statistic-card-container'
)}
style={css?.container?.style}
>
<div
class={twMerge(
'flex items-center justify-center w-12 h-12 border rounded-md p-2 text-black',
css?.media?.class,
'wm-statistic-card-media'
)}
style={css?.media?.style}
>
{#if isIcon}
{#if resolvedConfig.media && iconComponent}
<svelte:component this={iconComponent} size={24} />
{/if}
{:else}
<Loader loading={resolvedConfig.media.configuration.image.source == undefined}>
<img
on:pointerdown|preventDefault
src={getImageDataURL(
resolvedConfig?.media?.configuration?.image?.sourceKind,
resolvedConfig?.media?.configuration?.image?.source
)}
alt={resolvedConfig.title}
/>
</Loader>
{/if}
</div>
<div class="w-full">
<div class="text-base font-normal text-primary">{resolvedConfig.title}</div>
<div
class={twMerge(
'font-normal text-primary leading-none',
css?.title?.class,
'wm-statistic-card-title'
)}
style={css?.title?.style}
>
{resolvedConfig.title}
</div>
<div class="mt-1 flex items-baseline justify-between">
<div class="flex items-baseline text-2xl font-semibold text-blue-600 dark:text-blue-200">
<div
class={twMerge(
'flex items-baseline text-2xl leading-none font-semibold text-blue-600 dark:text-blue-200',
css?.value?.class,
'wm-statistic-card-value'
)}
style={css?.value?.style}
>
{resolvedConfig?.value}
</div>
@@ -2721,7 +2721,22 @@ This is a paragraph.
icon: FileBarChart,
documentationLink: `${documentationBaseUrl}/statistic_card`,
dims: '2:4-3:4' as AppComponentDimensions,
customCss: {},
customCss: {
title: {
class: '',
style: '',
quickCss: ['font-size: 1rem', 'font-size: 1.5rem', 'font-size: 2rem'],
quickTailwindClasses: ['text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl']
},
container: { class: '', style: '' },
value: {
class: '',
style: '',
quickCss: ['font-size: 1rem', 'font-size: 1.5rem', 'font-size: 2rem'],
quickTailwindClasses: ['text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl']
},
media: { class: '', style: '' }
},
initialData: {
configuration: {
title: {
@@ -2739,11 +2754,40 @@ This is a paragraph.
value: 0,
fieldType: 'number'
},
icon: {
type: 'static',
value: undefined,
fieldType: 'icon-select'
}
media: {
type: 'oneOf',
selected: 'image',
labels: {
icon: 'Icon',
image: 'Image'
},
configuration: {
icon: {
icon: {
type: 'static',
value: undefined,
fieldType: 'icon-select'
}
},
image: {
source: {
type: 'static',
value: '/logo.svg',
fieldType: 'text',
fileUpload: {
accept: 'image/*',
convertTo: 'base64'
}
},
sourceKind: {
fieldType: 'select',
type: 'static',
selectOptions: selectOptions.imageSourceKind,
value: 'url' as (typeof selectOptions.imageSourceKind)[number]
}
}
}
} as const
}
}
}
@@ -47,173 +47,207 @@
}
let dynamicClass: boolean = value?.evalClass !== undefined
let render = 0
</script>
<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}
<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}
{#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}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
copyToClipboard(wmClass)
dispatch('left')
}}
>
<Copy size={14} />
<div class="flex flex-row gap-2 text-2xs items-center">
<MoveLeft size={14} />
Copy for this component
</div>
</Button>
</div>
</Badge>
{/if}
</div>
</div>
{#if value}
<div class="p-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="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"
/>
{/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>
<div class="flex flex-col gap-1">
{#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>
{/if}
</div>
</div>
</label>
{#if quickStyleProperties?.length && isQuickMenuOpen}
<div transition:fade|local={{ duration: 200 }} class="w-full pt-1">
<QuickStyleMenu
bind:value={value.style}
properties={quickStyleProperties}
{componentType}
componentProperty={name}
/>
</div>
</Button>
{/if}
</div>
{/if}
{#if value.class !== undefined || forceClass}
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="block">
<div class="text-xs font-medium text-tertiary">
Tailwind classes
<Tooltip light documentationLink="https://tailwindcss.com/">
Use any tailwind classes to style your component
</Tooltip>
</div>
<div class="relative">
<SimpleEditor
class="h-24"
lang="tailwindcss"
bind:code={value.class}
fixedOverflowWidgets={true}
small
automaticLayout
deno={false}
subType="tailwind"
/>
</div>
</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'
}
} else {
value.evalClass = undefined
}
}}
/>
{#if value?.evalClass && dynamicClass}
<CssEval bind:evalClass={value.evalClass} />
{/if}
{#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>
{/if}
</div>
</div>
{/if}
{#if value}
<div class="p-2 flex flex-col">
{#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="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">
{#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>
{/if}
</div>
</div>
</label>
{#if quickStyleProperties?.length && isQuickMenuOpen}
<div transition:fade|local={{ duration: 200 }} class="w-full pt-1">
<QuickStyleMenu
bind:value={value.style}
properties={quickStyleProperties}
{componentType}
componentProperty={name}
/>
</div>
{/if}
{#if value.quickCss}
<div class="flex flex-row gap-1 items-center mt-1 flex-wrap">
{#each value.quickCss as v}
<Badge
small
baseClass="cursor-pointer"
on:click={() => {
value.style = value.style === '' ? `${v};` : `${value.style} ${v};`
}}
>
{v}
</Badge>
{/each}
</div>
{/if}
</div>
{/if}
{#if value.class !== undefined || forceClass}
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="block">
<div class="text-xs font-medium text-tertiary">
Tailwind classes
<Tooltip light documentationLink="https://tailwindcss.com/">
Use any tailwind classes to style your component
</Tooltip>
</div>
<div class="relative">
<SimpleEditor
class="h-24"
lang="tailwindcss"
bind:code={value.class}
fixedOverflowWidgets={true}
small
automaticLayout
deno={false}
subType="tailwind"
/>
</div>
{#if value.quickTailwindClasses}
<div class="flex flex-row gap-1 items-center mt-1 flex-wrap">
{#each value.quickTailwindClasses as cls}
<Badge
baseClass="cursor-pointer"
small
on:click={() => {
value.class = value.class === '' ? cls : `${value.class} ${cls}`
render++
}}
>
{cls}
</Badge>
{/each}
</div>
{/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'
}
} else {
value.evalClass = undefined
}
}}
/>
{#if value?.evalClass && dynamicClass}
<CssEval bind:evalClass={value.evalClass} />
{/if}
</div>
{/if}
{/key}
@@ -89,6 +89,7 @@
placeholder={config?.['placeholder']}
customTitle={config?.['customTitle']}
tooltip={config?.['tooltip']}
fileUpload={config?.['fileUpload']}
/>
{/if}
{/each}
+3 -1
View File
@@ -44,6 +44,8 @@ export type ComponentCssProperty = {
class?: string
style?: string
evalClass?: RichConfiguration
quickCss?: readonly string[] | undefined
quickTailwindClasses?: readonly string[] | undefined
}
export type ComponentCustomCSS<T extends keyof typeof components> = Partial<
@@ -173,7 +175,7 @@ export type ListContext = Writable<{
disabled: boolean
}>
export type ListInputs = {set: (id: string, value: any) => void, remove: (id: string) => void}
export type ListInputs = { set: (id: string, value: any) => void; remove: (id: string) => void }
export type GroupContext = Writable<Record<string, any>>
+17
View File
@@ -365,3 +365,20 @@ export function transformBareBase64IfNecessary(source: string | undefined) {
return `data:application/octet-stream;base64,${source}`
}
}
export function getImageDataURL(imageKind: string | undefined, image: string | undefined) {
if (!imageKind || !image) {
return null
}
switch (imageKind) {
case 'png encoded as base64':
return 'data:image/png;base64,' + image
case 'jpeg encoded as base64':
return 'data:image/jpeg;base64,' + image
case 'svg encoded as base64':
return 'data:image/svg+xml;base64,' + image
default:
return image
}
}