mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-30 16:17:25 +00:00
feat(frontend): Add quick style settings to app editor (#1308)
* feat(frontend): Add app secondary settings menu * refactor(frontend): Separate color picker * save * feat(frontend): Add quick style options * fix(frontend): Handle overflow * feat(frontend): Add suggestions to quick styles * save wip * feat(frontend): Add UI for quick styling * fix(frontend): Handle multi value properties * fix(frontend): Convenience updates * feat(frontend): Add styling properties to components * fix(frontend): Parse inner colors * fix(frontend): Multi value sync * fix(frontend): Correct unit handling * fix(frontend): Correct multivalue handling * remove comments * fix color picker label * feat(frontend): Add box-shadow property * feat(frontend): Add concise unit selector * feat(frontend): Update app labels * remove unused imports * fix width * fix(frontend): App freezing * fix(frontend): Remove unused imports * fix(frontend): Conditional chaining * fix * revert * minor updates * feat(frontend): Add 'apply style to all' button * fix(frontend): Update styling properties * fix(frontend): Styling menu typography update
This commit is contained in:
Generated
+31
@@ -66,6 +66,7 @@
|
||||
"prettier": "^2.8.3",
|
||||
"prettier-plugin-svelte": "^2.9.0",
|
||||
"simple-svelte-autocomplete": "^2.5.1",
|
||||
"style-to-object": "^0.4.1",
|
||||
"stylelint-config-recommended": "^9.0.0",
|
||||
"svelte": "^3.55.1",
|
||||
"svelte-awesome": "^3.0.0",
|
||||
@@ -4079,6 +4080,12 @@
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/inline-style-parser": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
|
||||
"integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
@@ -6959,6 +6966,15 @@
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/style-to-object": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.1.tgz",
|
||||
"integrity": "sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"inline-style-parser": "0.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/stylehacks": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
|
||||
@@ -11127,6 +11143,12 @@
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"dev": true
|
||||
},
|
||||
"inline-style-parser": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
|
||||
"integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==",
|
||||
"dev": true
|
||||
},
|
||||
"internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
@@ -13222,6 +13244,15 @@
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"style-to-object": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.1.tgz",
|
||||
"integrity": "sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"inline-style-parser": "0.1.1"
|
||||
}
|
||||
},
|
||||
"stylehacks": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"prettier": "^2.8.3",
|
||||
"prettier-plugin-svelte": "^2.9.0",
|
||||
"simple-svelte-autocomplete": "^2.5.1",
|
||||
"style-to-object": "^0.4.1",
|
||||
"stylelint-config-recommended": "^9.0.0",
|
||||
"svelte": "^3.55.1",
|
||||
"svelte-awesome": "^3.0.0",
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
import TabContent from '$lib/components/common/tabs/TabContent.svelte'
|
||||
import { Alert, Button, Tab } from '$lib/components/common'
|
||||
import ComponentList from './componentsPanel/ComponentList.svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { faCode, faPlus, faSliders } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import ContextPanel from './contextPanel/ContextPanel.svelte'
|
||||
import { classNames, encodeState } from '$lib/utils'
|
||||
import AppPreview from './AppPreview.svelte'
|
||||
@@ -41,6 +40,8 @@
|
||||
import ComponentNavigation from './component/ComponentNavigation.svelte'
|
||||
import ItemPicker from '$lib/components/ItemPicker.svelte'
|
||||
import VariableEditor from '$lib/components/VariableEditor.svelte'
|
||||
import { SecondaryMenu } from './settingsPanel/secondaryMenu'
|
||||
import { Component, Paintbrush, Plus } from 'lucide-svelte'
|
||||
|
||||
export let app: App
|
||||
export let path: string
|
||||
@@ -192,7 +193,7 @@
|
||||
<Pane size={15} minSize={5} maxSize={33}>
|
||||
<ContextPanel />
|
||||
</Pane>
|
||||
<Pane size={64}>
|
||||
<Pane size={63}>
|
||||
<SplitPanesWrapper>
|
||||
<Splitpanes horizontal>
|
||||
<Pane size={70}>
|
||||
@@ -233,31 +234,32 @@
|
||||
</Splitpanes>
|
||||
</SplitPanesWrapper>
|
||||
</Pane>
|
||||
<Pane size={21} minSize={5} maxSize={33}>
|
||||
<Pane size={22} minSize={5} maxSize={33}>
|
||||
<div class="relative flex flex-col h-full">
|
||||
<Tabs bind:selected={selectedTab} wrapperClass="!h-[40px]" class="!h-full">
|
||||
<Tab value="insert" size="xs">
|
||||
<div class="m-1 center-center gap-2">
|
||||
<Icon data={faPlus} />
|
||||
<div class="m-1 center-center gap-1">
|
||||
<Plus size={18} />
|
||||
<span>Insert</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab value="settings" size="xs">
|
||||
<div class="m-1 center-center gap-2">
|
||||
<Icon data={faSliders} />
|
||||
<span>Settings</span>
|
||||
<div class="m-1 center-center gap-1">
|
||||
<Component size={18} />
|
||||
<span>Component</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab value="css" size="xs">
|
||||
<div class="m-1 center-center gap-2">
|
||||
<Icon data={faCode} />
|
||||
<span>CSS</span>
|
||||
<div class="m-1 center-center gap-1">
|
||||
<Paintbrush size={18} />
|
||||
<span>Styling</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<div slot="content" class="h-full overflow-y-auto">
|
||||
<TabContent class="overflow-auto h-full" value="settings">
|
||||
{#if $selectedComponent !== undefined}
|
||||
<SettingsPanel />
|
||||
<SecondaryMenu />
|
||||
{:else}
|
||||
<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
|
||||
|
||||
@@ -100,8 +100,7 @@
|
||||
)}
|
||||
>
|
||||
<div
|
||||
class="w-full sticky top-0 flex justify-between border-b bg-gray-50 px-4 py-1 items-center gap-4"
|
||||
style="z-index: 1000;"
|
||||
class="w-full sticky z-[1001] top-0 flex justify-between border-b bg-gray-50 px-4 py-1 items-center gap-4"
|
||||
>
|
||||
<h2 class="truncate">{summary}</h2>
|
||||
<RecomputeAllComponents />
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
1. Add the component to one of the component sets: `layout`, `buttons`,
|
||||
`inputs` or `display` _(this controls which group the component will be
|
||||
placed in in the **Insert** menu)_
|
||||
1. [**`quickStyleProperties.ts`**](../componentsPanel/quickStyleProperties.ts):
|
||||
1. Add the component to the `quickStyleProperties` record
|
||||
1. _(optional)_ Add the CSS properties that could be applied to the component
|
||||
parts
|
||||
1. [**`Component.svelte`**](./Component.svelte):
|
||||
1. Add the new component in the Svelte `if` statement
|
||||
1. [**`default-codes.ts`**](./default-codes.ts):
|
||||
|
||||
@@ -1,71 +1,119 @@
|
||||
<script lang="ts">
|
||||
import { X } from 'lucide-svelte'
|
||||
import { Forward, Paintbrush2 } from 'lucide-svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { addWhitespaceBeforeCapitals } from '../../../../utils'
|
||||
import type { ComponentCssProperty } from '../../types'
|
||||
import { addWhitespaceBeforeCapitals, sendUserToast } from '../../../../utils'
|
||||
import { Button, ClearableInput } from '../../../common'
|
||||
import Popover from '../../../Popover.svelte'
|
||||
import type { AppViewerContext, ComponentCssProperty } from '../../types'
|
||||
import type { AppComponent } from '../component/components'
|
||||
import QuickStyleMenu from './QuickStyleMenu.svelte'
|
||||
import type { StylePropertyKey } from './quickStyleProperties'
|
||||
|
||||
export let name: string
|
||||
export let componentType: AppComponent['type'] | undefined = undefined
|
||||
export let value: ComponentCssProperty = {}
|
||||
export let forceStyle: boolean = false
|
||||
export let forceClass: boolean = false
|
||||
export let quickStyleProperties: StylePropertyKey[] | undefined = undefined
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const dispatch = createEventDispatcher()
|
||||
let isQuickMenuOpen = false
|
||||
|
||||
function reset(property: keyof ComponentCssProperty) {
|
||||
if (value) {
|
||||
value[property] = ''
|
||||
$: dispatch('change', value)
|
||||
|
||||
function toggleQuickMenu() {
|
||||
isQuickMenuOpen = !isQuickMenuOpen
|
||||
}
|
||||
|
||||
function applyToAllInstances() {
|
||||
if (
|
||||
componentType &&
|
||||
componentType in ($app?.css || {}) &&
|
||||
name in ($app?.css?.[componentType] || {})
|
||||
) {
|
||||
$app.css![componentType]![name].style = value.style
|
||||
sendUserToast(
|
||||
`Applied style to all instances of the ${componentType.replace('component', '')} component`
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="text-sm font-semibold text-gray-500 capitalize pt-2">
|
||||
<div
|
||||
class="sticky top-0 z-20 text-lg bg-white font-semibold [font-variant:small-caps] text-gray-700 pt-2 pb-1"
|
||||
>
|
||||
{addWhitespaceBeforeCapitals(name)}
|
||||
</div>
|
||||
{#if value}
|
||||
<div class="border-l border-gray-400/80 py-1 pl-3.5 mt-1 ml-0.5">
|
||||
<div class="border-l border-gray-400/80 py-1 pl-3.5 ml-0.5">
|
||||
{#if value.style !== undefined || forceStyle}
|
||||
<label class="block pb-2">
|
||||
<div class="text-xs font-medium pb-0.5"> Style </div>
|
||||
<div class="relative">
|
||||
<input
|
||||
on:keydown|stopPropagation
|
||||
type="text"
|
||||
class="!pr-7"
|
||||
bind:value={value.style}
|
||||
on:focus
|
||||
/>
|
||||
{#if value?.style}
|
||||
<button
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
class="absolute z-10 top-1.5 right-1 rounded-full p-1 duration-200 hover:bg-gray-200"
|
||||
aria-label="Remove styles"
|
||||
on:click|preventDefault|stopPropagation={() => reset('style')}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</label>
|
||||
<div class="pb-2">
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label class="block">
|
||||
<div class="text-sm font-medium text-gray-600 pb-0.5"> Style </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"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
{#if componentType}
|
||||
<Popover placement="bottom" notClickable disapperTimoout={0}>
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
btnClasses="!p-1 !w-[34px] !h-[34px]"
|
||||
aria-label="Apply to all instances of this component"
|
||||
on:click={applyToAllInstances}
|
||||
>
|
||||
<Forward size={18} />
|
||||
</Button>
|
||||
<svelte:fragment slot="text">
|
||||
Apply to all instances of this component
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if quickStyleProperties?.length}
|
||||
<Popover placement="bottom" notClickable disapperTimoout={0}>
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
btnClasses="!p-1 !w-[34px] !h-[34px] {isQuickMenuOpen
|
||||
? '!bg-gray-200/60 hover:!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} />
|
||||
</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 pb-0.5"> Tailwind classes </div>
|
||||
<div class="text-sm font-medium text-gray-600 pb-0.5"> Tailwind classes </div>
|
||||
<div class="relative">
|
||||
<input
|
||||
on:keydown|stopPropagation
|
||||
type="text"
|
||||
class="!pr-7"
|
||||
bind:value={value.class}
|
||||
on:focus
|
||||
/>
|
||||
{#if value?.class}
|
||||
<button
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
class="absolute z-10 top-1.5 right-1 rounded-full p-1 duration-200 hover:bg-gray-200"
|
||||
aria-label="Remove classes"
|
||||
on:click|preventDefault|stopPropagation={() => reset('class')}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
<ClearableInput bind:value={value.class} />
|
||||
</div>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount, setContext } from 'svelte'
|
||||
import parse from 'style-to-object'
|
||||
import { isValidHexColor } from '../../../../utils'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import {
|
||||
createStyleStore,
|
||||
StylePropertyType,
|
||||
StylePropertyUnits,
|
||||
STYLE_STORE_KEY,
|
||||
type StylePropertyKey,
|
||||
type TopColors
|
||||
} from './quickStyleProperties'
|
||||
import QuickStyleProperty from './QuickStyleProperty.svelte'
|
||||
|
||||
export let value = ''
|
||||
export let properties: StylePropertyKey[]
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const styleStore = createStyleStore(properties)
|
||||
setContext(STYLE_STORE_KEY, styleStore)
|
||||
let multiValues: Record<number, string[]> = initiateMultiValues()
|
||||
let mounted = false
|
||||
|
||||
$: mounted && $styleStore && writeStyle()
|
||||
$: mounted && (!value || value) && parseStyle()
|
||||
$: $app && setTopColors()
|
||||
|
||||
function parseStyle() {
|
||||
styleStore.resetStyle()
|
||||
if (!value) {
|
||||
multiValues = initiateMultiValues()
|
||||
return
|
||||
}
|
||||
const current = parse(value) || {}
|
||||
Object.entries(current).forEach(([k, v]) => {
|
||||
styleStore.updatePropValue(k as StylePropertyKey, v)
|
||||
const { prop, index } = styleStore.getProp(k as StylePropertyKey)
|
||||
if (Array.isArray(prop?.prop?.value) && index !== undefined) {
|
||||
const valueArray = v.split(' ')
|
||||
multiValues[index] = multiValues[index].map((v, i) => valueArray[i] || v)
|
||||
}
|
||||
})
|
||||
setTopColors()
|
||||
}
|
||||
|
||||
function writeStyle() {
|
||||
const current = parse(value) || {}
|
||||
$styleStore.style.forEach((s) => {
|
||||
current[s.prop.key] = convertValue(s.value)
|
||||
})
|
||||
const entries = Object.entries(current)
|
||||
value = entries.reduce((style, [k, v]) => {
|
||||
return v ? `${style} ${k}: ${v}; `.trim() : style
|
||||
}, '')
|
||||
}
|
||||
|
||||
function convertValue(value: any) {
|
||||
switch (typeof value) {
|
||||
case 'number':
|
||||
return value + ''
|
||||
case 'string':
|
||||
return value
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function setTopColors() {
|
||||
const styles = collectStyles()
|
||||
const parsedStyles = styles.map((style) => parse(style) || {})
|
||||
const usedColors: Record<string, number> = {}
|
||||
parsedStyles.forEach((style) => {
|
||||
Object.values(style).reduce((colors, v) => {
|
||||
// TODO: support RGB and HSL colors as well
|
||||
|
||||
// Splitting is needed so colors can be extracted
|
||||
// from values like '1px solid #000000'
|
||||
v.split(' ').forEach((v) => {
|
||||
if (isValidHexColor(v)) {
|
||||
colors[v] = (colors[v] || 0) + 1
|
||||
}
|
||||
})
|
||||
return colors
|
||||
}, usedColors)
|
||||
})
|
||||
|
||||
const colors = Object.entries(usedColors)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 3)
|
||||
.map(([color]) => color) as TopColors
|
||||
styleStore.setTopColors(colors)
|
||||
}
|
||||
|
||||
function collectStyles() {
|
||||
const styles: string[] = []
|
||||
// Getting global app styles
|
||||
Object.values($app.css || {}).forEach((element) => {
|
||||
Object.values(element).filter(({ style }) => style && styles.push(style))
|
||||
})
|
||||
// Getting styles from individual components
|
||||
$app.grid.map((component) => {
|
||||
Object.values(component.data.customCss || {}).forEach(({ style }) => {
|
||||
style && styles.push(style)
|
||||
})
|
||||
})
|
||||
// Getting style from subgrids
|
||||
Object.values($app.subgrids || {}).forEach((grid) => {
|
||||
grid.map((component) => {
|
||||
Object.values(component.data.customCss || {}).forEach(({ style }) => {
|
||||
style && styles.push(style)
|
||||
})
|
||||
})
|
||||
})
|
||||
return styles
|
||||
}
|
||||
|
||||
function initiateMultiValues() {
|
||||
return $styleStore.style.reduce((acc, curr, i) => {
|
||||
if (Array.isArray(curr.prop.value)) {
|
||||
acc[i] = Array.from({ length: curr.prop.value.length }, () => {
|
||||
return ''
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
function setMultiValueProperty(index: number) {
|
||||
if (multiValues[index].every((v) => !v || +v === 0 || StylePropertyUnits.includes(v))) {
|
||||
$styleStore.style[index].value = ''
|
||||
return
|
||||
}
|
||||
const values = multiValues[index].map((v, i) => {
|
||||
v = StylePropertyUnits.includes(v) ? '' : v
|
||||
const type = $styleStore.style[index].prop.value[i].type
|
||||
if (v) {
|
||||
multiValues[index][i] = v
|
||||
return v
|
||||
} else if (type === StylePropertyType.color) {
|
||||
return '#000000'
|
||||
} else if (type === StylePropertyType.number) {
|
||||
return 0
|
||||
} else if (type === StylePropertyType.unit) {
|
||||
return 0
|
||||
} else if (type === StylePropertyType.text) {
|
||||
const options = $styleStore.style[index].prop.value[i].options
|
||||
return options ? options[0].text : ''
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
$styleStore.style[index].value = values.join(' ').trim()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
parseStyle()
|
||||
mounted = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="bg-gray-200/60 rounded-md p-2">
|
||||
{#each $styleStore.style as { prop }, index}
|
||||
<div class="pb-3 last:pb-0">
|
||||
<div class="text-sm font-medium text-gray-600 pb-0.5"> {prop.key} </div>
|
||||
<div class="flex items-center gap-1 w-full">
|
||||
{#if Array.isArray(prop.value)}
|
||||
<div class="flex justify-start items-center flex-wrap gap-x-4 gap-y-1">
|
||||
{#each prop.value as value, i}
|
||||
<QuickStyleProperty
|
||||
prop={{ ...prop, value }}
|
||||
inline
|
||||
bind:value={multiValues[index][i]}
|
||||
on:change={() => setMultiValueProperty(index)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<QuickStyleProperty {prop} bind:value={$styleStore.style[index].value} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,141 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { Button, ClearableInput, Menu } from '../../../common'
|
||||
import Popover from '../../../Popover.svelte'
|
||||
import ColorInput from '../settingsPanel/inputEditor/ColorInput.svelte'
|
||||
import {
|
||||
StylePropertyType,
|
||||
StylePropertyUnits,
|
||||
STYLE_STORE_KEY,
|
||||
type StyleStore,
|
||||
type StyleStoreValue
|
||||
} from './quickStyleProperties'
|
||||
|
||||
export let prop: StyleStoreValue['style'][number]['prop']
|
||||
export let value: string | undefined
|
||||
export let inline = false
|
||||
const styleStore = getContext<StyleStore>(STYLE_STORE_KEY)
|
||||
const dispatch = createEventDispatcher()
|
||||
const key = prop.key
|
||||
const type = prop.value?.['type']
|
||||
let unit: (typeof StylePropertyUnits)[number] = StylePropertyUnits[0]
|
||||
let internalValue: number | string
|
||||
|
||||
$: internalValue = value ? +value.replace(unit, '') : ''
|
||||
$: dispatch('change', value)
|
||||
|
||||
function updateValue(next: number) {
|
||||
value = next ? next + unit : ''
|
||||
}
|
||||
|
||||
function updateUnit(next: (typeof StylePropertyUnits)[number]) {
|
||||
value = value?.replace(unit, next) || ''
|
||||
unit = next
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={inline && type !== StylePropertyType.color ? '' : 'w-full'}>
|
||||
{#if prop.value['title']}
|
||||
<div class="font-medium text-xs text-gray-500">
|
||||
{prop.value['title']}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex gap-1">
|
||||
{#if type === StylePropertyType.color}
|
||||
<ColorInput bind:value />
|
||||
{#each $styleStore.topColors as color}
|
||||
<Popover placement="bottom" notClickable disapperTimoout={0} class="flex">
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
btnClasses="!p-0 !w-[34px] !h-[34px]"
|
||||
aria-label="Set {key} to {color}"
|
||||
style={`background-color: ${color};`}
|
||||
on:click={() => (value = color)}
|
||||
/>
|
||||
<svelte:fragment slot="text">{color}</svelte:fragment>
|
||||
</Popover>
|
||||
{/each}
|
||||
{:else if type === StylePropertyType.number}
|
||||
<ClearableInput
|
||||
type="number"
|
||||
bind:value
|
||||
step={prop.value?.['step'] || 1}
|
||||
min={prop.value?.['min']}
|
||||
max={prop.value?.['max']}
|
||||
/>
|
||||
{:else if type === StylePropertyType.unit}
|
||||
<ClearableInput
|
||||
wrapperClass="flex items-center {inline ? '!grow-0' : ''}"
|
||||
inputClass="!border-r-0 !rounded-r-none {inline ? '!w-[90px]' : ''}"
|
||||
buttonClass="!right-9"
|
||||
type="number"
|
||||
value={internalValue}
|
||||
on:change={({ detail }) => updateValue(detail)}
|
||||
>
|
||||
<Menu
|
||||
let:close
|
||||
noMinW
|
||||
wrapperClasses="h-full bg-white rounded-r-md border-y border-r border-gray-300 pr-0.5"
|
||||
popupClasses="!mt-0"
|
||||
>
|
||||
<button
|
||||
slot="trigger"
|
||||
type="button"
|
||||
class="font-normal text-xs px-1 py-1.5 w-8 rounded mt-0.5 duration-200 hover:bg-gray-200/90"
|
||||
>
|
||||
{unit}
|
||||
</button>
|
||||
<ul class="bg-white rounded border py-1 overflow-auto">
|
||||
{#each StylePropertyUnits as u}
|
||||
<li class="w-full">
|
||||
<Button
|
||||
type="button"
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="contained"
|
||||
btnClasses="!justify-start !rounded-none !w-full !px-3 !py-1.5"
|
||||
on:click={() => {
|
||||
updateUnit(u)
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{u}
|
||||
</Button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</Menu>
|
||||
</ClearableInput>
|
||||
{:else if type === StylePropertyType.text}
|
||||
{#each prop.value?.['options'] || [] as option}
|
||||
<Popover placement="bottom" notClickable disapperTimoout={0}>
|
||||
<Button
|
||||
color={value === option.text ? 'dark' : 'light'}
|
||||
size="xs"
|
||||
variant={value === option.text ? 'contained' : 'border'}
|
||||
btnClasses="!p-1 !min-w-[34px] !h-[34px]"
|
||||
aria-label="Set {key} to {option.text}"
|
||||
on:click={() => {
|
||||
if (value === option.text) {
|
||||
value = ''
|
||||
} else {
|
||||
value = option.text
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if typeof option.icon === 'string'}
|
||||
{option.icon}
|
||||
{:else}
|
||||
<svelte:component this={option.icon} size={18} />
|
||||
{/if}
|
||||
</Button>
|
||||
<svelte:fragment slot="text">{option.text}</svelte:fragment>
|
||||
</Popover>
|
||||
{:else}
|
||||
<ClearableInput inputClass="min-w-[32px]" bind:value />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,644 @@
|
||||
import { get, writable } from 'svelte/store'
|
||||
import {
|
||||
AlignCenter,
|
||||
AlignJustify,
|
||||
AlignLeft,
|
||||
AlignRight,
|
||||
Asterisk,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Grid,
|
||||
Italic,
|
||||
Mouse,
|
||||
MousePointer,
|
||||
Pointer,
|
||||
RectangleHorizontal,
|
||||
Slash,
|
||||
Square,
|
||||
StretchVertical,
|
||||
Strikethrough,
|
||||
Type,
|
||||
Underline
|
||||
} from 'lucide-svelte'
|
||||
import type { components } from '../component'
|
||||
|
||||
export const STYLE_STORE_KEY = 'style_store' as const
|
||||
|
||||
export type StyleStore = ReturnType<typeof createStyleStore>
|
||||
export type StyleStoreValue = {
|
||||
style: {
|
||||
prop: (typeof StyleProperty)[number]
|
||||
value: string | undefined
|
||||
}[]
|
||||
topColors: TopColors
|
||||
}
|
||||
|
||||
export function createStyleStore(properties: StylePropertyKey[]) {
|
||||
const style = StyleProperty.filter((p) => properties.includes(p.key)).map((p) => ({
|
||||
prop: p,
|
||||
value: '' as string | undefined
|
||||
}))
|
||||
const store = writable<StyleStoreValue>({
|
||||
style: [...style],
|
||||
topColors: [] as TopColors
|
||||
})
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
set: store.set,
|
||||
update: store.update,
|
||||
updatePropValue: (key: StylePropertyKey, value?: string) => {
|
||||
if (!key) {
|
||||
return
|
||||
}
|
||||
store.update((s) => {
|
||||
const index = s.style.findIndex((p) => p.prop.key === key)
|
||||
if (index !== -1) {
|
||||
s.style[index].value = value || ''
|
||||
}
|
||||
return s
|
||||
})
|
||||
},
|
||||
getProp(key: StylePropertyKey) {
|
||||
const s = get(store)
|
||||
const index = s.style.findIndex((p) => p.prop.key === key)
|
||||
return {
|
||||
prop: s.style[index],
|
||||
index: index === -1 ? undefined : index
|
||||
}
|
||||
},
|
||||
resetStyle: () => {
|
||||
store.update((s) => {
|
||||
s.style = style.map((s) => ({ ...s, value: '' }))
|
||||
return s
|
||||
})
|
||||
},
|
||||
setTopColors: (colors: TopColors) => {
|
||||
store.update((s) => {
|
||||
s.topColors = colors
|
||||
return s
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export enum StylePropertyType {
|
||||
'color' = 'color', // color value
|
||||
'unit' = 'unit', // number with unit like px, em, rem, etc.
|
||||
'number' = 'number', // bare number like the value of 'font-weight'
|
||||
'text' = 'text' // text like the value of 'display'
|
||||
}
|
||||
|
||||
export const StylePropertyUnits = ['px', 'em', 'rem', '%', 'vh', 'vw']
|
||||
|
||||
export type TopColors = [] | [string] | [string, string] | [string, string, string]
|
||||
|
||||
export type StylePropertyKey = (typeof StyleProperty)[number]['key']
|
||||
|
||||
// Using an array instead of an object to preserve the order of the properties
|
||||
export const StyleProperty = [
|
||||
{
|
||||
key: 'display',
|
||||
value: {
|
||||
type: StylePropertyType.text,
|
||||
options: [
|
||||
{
|
||||
text: 'block',
|
||||
icon: Square
|
||||
},
|
||||
{
|
||||
text: 'inline-block',
|
||||
icon: RectangleHorizontal
|
||||
},
|
||||
{
|
||||
text: 'flex',
|
||||
icon: StretchVertical
|
||||
},
|
||||
{
|
||||
text: 'grid',
|
||||
icon: Grid
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'padding',
|
||||
value: [
|
||||
{
|
||||
type: StylePropertyType.unit,
|
||||
title: 'top'
|
||||
},
|
||||
{
|
||||
type: StylePropertyType.unit,
|
||||
title: 'right'
|
||||
},
|
||||
{
|
||||
type: StylePropertyType.unit,
|
||||
title: 'bottom'
|
||||
},
|
||||
{
|
||||
type: StylePropertyType.unit,
|
||||
title: 'left'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'opacity',
|
||||
value: {
|
||||
type: StylePropertyType.number,
|
||||
step: 0.1,
|
||||
min: 0,
|
||||
max: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'cursor',
|
||||
value: {
|
||||
type: StylePropertyType.text,
|
||||
options: [
|
||||
{
|
||||
text: 'default',
|
||||
icon: MousePointer
|
||||
},
|
||||
{
|
||||
text: 'pointer',
|
||||
icon: Pointer
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'width',
|
||||
value: {
|
||||
type: StylePropertyType.unit
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'min-width',
|
||||
value: {
|
||||
type: StylePropertyType.unit
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'max-width',
|
||||
value: {
|
||||
type: StylePropertyType.unit
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'height',
|
||||
value: {
|
||||
type: StylePropertyType.unit
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'min-height',
|
||||
value: {
|
||||
type: StylePropertyType.unit
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'max-height',
|
||||
value: {
|
||||
type: StylePropertyType.unit
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'border',
|
||||
value: [
|
||||
{
|
||||
type: StylePropertyType.unit,
|
||||
title: 'width'
|
||||
},
|
||||
{
|
||||
type: StylePropertyType.text,
|
||||
title: 'style',
|
||||
options: [
|
||||
{
|
||||
text: 'solid',
|
||||
icon: '___'
|
||||
},
|
||||
{
|
||||
text: 'dashed',
|
||||
icon: '_ _'
|
||||
},
|
||||
{
|
||||
text: 'dotted',
|
||||
icon: '. .'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: StylePropertyType.color,
|
||||
title: 'color'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'border-radius',
|
||||
value: {
|
||||
type: StylePropertyType.unit
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'box-shadow',
|
||||
value: [
|
||||
{
|
||||
type: StylePropertyType.unit,
|
||||
title: 'offset-x'
|
||||
},
|
||||
{
|
||||
type: StylePropertyType.unit,
|
||||
title: 'offset-y'
|
||||
},
|
||||
{
|
||||
type: StylePropertyType.unit,
|
||||
title: 'blur'
|
||||
},
|
||||
{
|
||||
type: StylePropertyType.unit,
|
||||
title: 'spread'
|
||||
},
|
||||
{
|
||||
type: StylePropertyType.color,
|
||||
title: 'color'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'background-color',
|
||||
value: {
|
||||
type: StylePropertyType.color
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'color',
|
||||
value: {
|
||||
type: StylePropertyType.color
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'font-size',
|
||||
value: {
|
||||
type: StylePropertyType.unit
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'font-family',
|
||||
value: {
|
||||
type: StylePropertyType.text
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'font-weight',
|
||||
value: {
|
||||
type: StylePropertyType.number
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'font-style',
|
||||
value: {
|
||||
type: StylePropertyType.text,
|
||||
options: [
|
||||
{
|
||||
text: 'normal',
|
||||
icon: Type
|
||||
},
|
||||
{
|
||||
text: 'italic',
|
||||
icon: Italic
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'text-align',
|
||||
value: {
|
||||
type: StylePropertyType.text,
|
||||
options: [
|
||||
{
|
||||
text: 'left',
|
||||
icon: AlignLeft
|
||||
},
|
||||
{
|
||||
text: 'center',
|
||||
icon: AlignCenter
|
||||
},
|
||||
{
|
||||
text: 'right',
|
||||
icon: AlignRight
|
||||
},
|
||||
{
|
||||
text: 'justify',
|
||||
icon: AlignJustify
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'text-decoration',
|
||||
value: {
|
||||
type: StylePropertyType.text,
|
||||
options: [
|
||||
{
|
||||
text: 'none',
|
||||
icon: Slash
|
||||
},
|
||||
{
|
||||
text: 'underline',
|
||||
icon: Underline
|
||||
},
|
||||
{
|
||||
text: 'line-through',
|
||||
icon: Strikethrough
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'text-transform',
|
||||
value: {
|
||||
type: StylePropertyType.text,
|
||||
options: [
|
||||
{
|
||||
text: 'none',
|
||||
icon: Slash
|
||||
},
|
||||
{
|
||||
text: 'capitalize',
|
||||
icon: 'Aa'
|
||||
},
|
||||
{
|
||||
text: 'uppercase',
|
||||
icon: 'AA'
|
||||
},
|
||||
{
|
||||
text: 'lowercase',
|
||||
icon: 'aa'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'line-height',
|
||||
value: {
|
||||
type: StylePropertyType.unit
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'letter-spacing',
|
||||
value: {
|
||||
type: StylePropertyType.unit
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'word-spacing',
|
||||
value: {
|
||||
type: StylePropertyType.unit
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'overflow',
|
||||
value: {
|
||||
type: StylePropertyType.text,
|
||||
options: [
|
||||
{
|
||||
text: 'auto',
|
||||
icon: Asterisk
|
||||
},
|
||||
{
|
||||
text: 'visible',
|
||||
icon: Eye
|
||||
},
|
||||
{
|
||||
text: 'hidden',
|
||||
icon: EyeOff
|
||||
},
|
||||
{
|
||||
text: 'scroll',
|
||||
icon: Mouse
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
] as const
|
||||
|
||||
const allDefaultProps = StyleProperty.map(({ key }) => key)
|
||||
|
||||
const containerDefaultProps: StylePropertyKey[] = [
|
||||
'padding',
|
||||
'opacity',
|
||||
'border',
|
||||
'border-radius',
|
||||
'box-shadow',
|
||||
'background-color',
|
||||
'overflow'
|
||||
]
|
||||
|
||||
const textDefaultProps: StylePropertyKey[] = [
|
||||
'padding',
|
||||
'opacity',
|
||||
'background-color',
|
||||
'color',
|
||||
'font-size',
|
||||
'font-family',
|
||||
'font-weight',
|
||||
'font-style',
|
||||
'text-align',
|
||||
'text-decoration',
|
||||
'text-transform',
|
||||
'line-height',
|
||||
'letter-spacing',
|
||||
'word-spacing'
|
||||
]
|
||||
|
||||
const sizeDefaultProps: StylePropertyKey[] = [
|
||||
'width',
|
||||
'min-width',
|
||||
'max-width',
|
||||
'height',
|
||||
'min-height',
|
||||
'max-height'
|
||||
]
|
||||
|
||||
const buttonDefaultProps: StylePropertyKey[] = [
|
||||
'cursor',
|
||||
'border',
|
||||
'border-radius',
|
||||
'box-shadow',
|
||||
...textDefaultProps
|
||||
]
|
||||
|
||||
const inputDefaultProps: StylePropertyKey[] = [
|
||||
'cursor',
|
||||
'border',
|
||||
'border-radius',
|
||||
...textDefaultProps
|
||||
]
|
||||
|
||||
export const quickStyleProperties: Record<
|
||||
keyof typeof components,
|
||||
Record<string, StylePropertyKey[]>
|
||||
> = {
|
||||
mapcomponent: {
|
||||
map: containerDefaultProps
|
||||
},
|
||||
pdfcomponent: {
|
||||
container: containerDefaultProps
|
||||
},
|
||||
formcomponent: {
|
||||
container: containerDefaultProps,
|
||||
button: buttonDefaultProps
|
||||
},
|
||||
htmlcomponent: {
|
||||
container: allDefaultProps
|
||||
},
|
||||
iconcomponent: {
|
||||
container: containerDefaultProps,
|
||||
icon: [
|
||||
'padding',
|
||||
'opacity',
|
||||
'cursor',
|
||||
'width',
|
||||
'min-width',
|
||||
'max-width',
|
||||
'height',
|
||||
'min-height',
|
||||
'max-height',
|
||||
'color'
|
||||
]
|
||||
},
|
||||
tabscomponent: {
|
||||
tabRow: containerDefaultProps,
|
||||
allTabs: [...textDefaultProps, ...sizeDefaultProps],
|
||||
selectedTab: [...textDefaultProps, ...sizeDefaultProps],
|
||||
container: containerDefaultProps
|
||||
},
|
||||
textcomponent: {
|
||||
text: textDefaultProps
|
||||
},
|
||||
imagecomponent: {
|
||||
image: containerDefaultProps
|
||||
},
|
||||
rangecomponent: {
|
||||
handles: ['opacity', 'cursor', 'border', 'border-radius', 'background-color'],
|
||||
bar: ['opacity', 'cursor', 'border', 'border-radius', 'background-color'],
|
||||
limits: textDefaultProps,
|
||||
values: textDefaultProps
|
||||
},
|
||||
tablecomponent: {
|
||||
tableHeader: containerDefaultProps,
|
||||
tableBody: containerDefaultProps,
|
||||
tableFooter: containerDefaultProps,
|
||||
container: containerDefaultProps
|
||||
},
|
||||
aggridcomponent: {},
|
||||
buttoncomponent: {
|
||||
button: buttonDefaultProps
|
||||
},
|
||||
drawercomponent: {
|
||||
container: containerDefaultProps
|
||||
},
|
||||
plotlycomponent: {},
|
||||
selectcomponent: {
|
||||
input: inputDefaultProps
|
||||
},
|
||||
slidercomponent: {
|
||||
handles: ['opacity', 'cursor', 'border', 'border-radius', 'background-color'],
|
||||
bar: ['opacity', 'cursor', 'border', 'border-radius', 'background-color'],
|
||||
limits: textDefaultProps,
|
||||
values: textDefaultProps
|
||||
},
|
||||
displaycomponent: {
|
||||
header: [...containerDefaultProps, ...textDefaultProps],
|
||||
container: containerDefaultProps
|
||||
},
|
||||
barchartcomponent: {
|
||||
container: containerDefaultProps
|
||||
},
|
||||
checkboxcomponent: {
|
||||
text: textDefaultProps
|
||||
},
|
||||
currencycomponent: {
|
||||
input: inputDefaultProps
|
||||
},
|
||||
piechartcomponent: {
|
||||
container: containerDefaultProps
|
||||
},
|
||||
vegalitecomponent: {},
|
||||
containercomponent: {
|
||||
container: containerDefaultProps
|
||||
},
|
||||
dateinputcomponent: {
|
||||
input: inputDefaultProps
|
||||
},
|
||||
fileinputcomponent: {
|
||||
container: containerDefaultProps
|
||||
},
|
||||
textinputcomponent: {
|
||||
input: inputDefaultProps
|
||||
},
|
||||
emailinputcomponent: {
|
||||
input: inputDefaultProps
|
||||
},
|
||||
formbuttoncomponent: {
|
||||
button: buttonDefaultProps,
|
||||
popup: containerDefaultProps
|
||||
},
|
||||
timeseriescomponent: {
|
||||
container: containerDefaultProps
|
||||
},
|
||||
multiselectcomponent: {
|
||||
input: inputDefaultProps
|
||||
},
|
||||
numberinputcomponent: {
|
||||
input: inputDefaultProps
|
||||
},
|
||||
scatterchartcomponent: {
|
||||
container: containerDefaultProps
|
||||
},
|
||||
passwordinputcomponent: {
|
||||
input: inputDefaultProps
|
||||
},
|
||||
resourceselectcomponent: {
|
||||
input: inputDefaultProps
|
||||
},
|
||||
verticaldividercomponent: {
|
||||
divider: [
|
||||
'padding',
|
||||
'opacity',
|
||||
'width',
|
||||
'min-width',
|
||||
'max-width',
|
||||
'height',
|
||||
'min-height',
|
||||
'max-height',
|
||||
'border',
|
||||
'border-radius',
|
||||
'background-color'
|
||||
],
|
||||
container: containerDefaultProps
|
||||
},
|
||||
horizontaldividercomponent: {
|
||||
divider: [
|
||||
'padding',
|
||||
'opacity',
|
||||
'width',
|
||||
'min-width',
|
||||
'max-width',
|
||||
'height',
|
||||
'min-height',
|
||||
'max-height',
|
||||
'border',
|
||||
'border-radius',
|
||||
'background-color'
|
||||
],
|
||||
container: containerDefaultProps
|
||||
},
|
||||
verticalsplitpanescomponent: {
|
||||
container: containerDefaultProps
|
||||
},
|
||||
horizontalsplitpanescomponent: {
|
||||
container: containerDefaultProps
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext, AppViewerContext, GridItem, RichConfiguration } from '../../types'
|
||||
import PanelSection from './common/PanelSection.svelte'
|
||||
@@ -9,7 +9,7 @@
|
||||
import StaticInputEditor from './inputEditor/StaticInputEditor.svelte'
|
||||
import ConnectedInputEditor from './inputEditor/ConnectedInputEditor.svelte'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import { capitalize, classNames, getModifierKey } from '$lib/utils'
|
||||
import { capitalize, classNames, getModifierKey, isMac } from '$lib/utils'
|
||||
import { buildExtraLib, fieldTypeToTsType } from '../../utils'
|
||||
import Recompute from './Recompute.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
@@ -17,7 +17,7 @@
|
||||
import AlignmentEditor from './AlignmentEditor.svelte'
|
||||
import RunnableInputEditor from './inputEditor/RunnableInputEditor.svelte'
|
||||
import TemplateEditor from '$lib/components/TemplateEditor.svelte'
|
||||
import { ccomponents, type AppComponent } from '../component'
|
||||
import { ccomponents } from '../component'
|
||||
import CssProperty from '../componentsPanel/CssProperty.svelte'
|
||||
import GridTab from './GridTab.svelte'
|
||||
import { clearErrorByComponentId, clearJobsByComponentId, deleteGridItem } from '../appUtils'
|
||||
@@ -25,6 +25,9 @@
|
||||
import { slide } from 'svelte/transition'
|
||||
import { push } from '$lib/history'
|
||||
import Kbd from '$lib/components/common/kbd/Kbd.svelte'
|
||||
import { secondaryMenu } from './secondaryMenu'
|
||||
import StylePanel from './StylePanel.svelte'
|
||||
import { Delete } from 'lucide-svelte'
|
||||
|
||||
export let componentSettings: { item: GridItem; parent: string | undefined } | undefined =
|
||||
undefined
|
||||
@@ -90,12 +93,10 @@
|
||||
: undefined
|
||||
|
||||
function keydown(event: KeyboardEvent) {
|
||||
switch (event.key) {
|
||||
case 'Delete': {
|
||||
removeGridElement()
|
||||
event.stopPropagation()
|
||||
break
|
||||
}
|
||||
const { key, metaKey } = event
|
||||
if (key === 'Delte' || (key === 'Backspace' && metaKey)) {
|
||||
removeGridElement()
|
||||
event.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,19 +236,20 @@
|
||||
<div class="grow shrink" />
|
||||
|
||||
{#if Object.keys(ccomponents[component.type].customCss ?? {}).length > 0}
|
||||
<PanelSection title="Custom CSS">
|
||||
<PanelSection title="Styling">
|
||||
<div slot="action">
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
endIcon={{ icon: viewCssOptions ? faChevronUp : faChevronDown }}
|
||||
on:click={() => (viewCssOptions = !viewCssOptions)}
|
||||
variant="border"
|
||||
endIcon={{ icon: faChevronRight }}
|
||||
on:click={() => secondaryMenu.open(StylePanel, { component })}
|
||||
>
|
||||
{viewCssOptions ? 'Hide' : 'Show'}
|
||||
Open
|
||||
</Button>
|
||||
</div>
|
||||
{#if viewCssOptions}
|
||||
<div transition:slide|local>
|
||||
<div transition:slide|local class="w-full">
|
||||
{#each Object.keys(ccomponents[component.type].customCss ?? {}) as name}
|
||||
{#if componentSettings.item.data?.customCss != undefined}
|
||||
<div class="w-full mb-2">
|
||||
@@ -270,7 +272,15 @@
|
||||
<div slot="action">
|
||||
<Button size="xs" color="red" variant="border" on:click={removeGridElement}>
|
||||
Delete
|
||||
<Kbd>Del</Kbd>
|
||||
{#if isMac()}
|
||||
<Kbd kbdClass="center-center">
|
||||
<span class="text-lg leading-none">⌘</span>
|
||||
<span class="px-0.5">+</span>
|
||||
<Delete size={16} />
|
||||
</Kbd>
|
||||
{:else}
|
||||
<Kbd>Del</Kbd>
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import { ccomponents, type AppComponent } from '../component'
|
||||
import CssProperty from '../componentsPanel/CssProperty.svelte'
|
||||
import { quickStyleProperties } from '../componentsPanel/quickStyleProperties'
|
||||
|
||||
export let component: AppComponent | undefined
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
</script>
|
||||
|
||||
{#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}
|
||||
{name}
|
||||
componentType={component.type}
|
||||
bind:value={component.customCss[name]}
|
||||
on:change={() => app.set($app)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
+12
-12
@@ -1,22 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import ColorPicker, { ChromeVariant } from 'svelte-awesome-color-picker'
|
||||
import type { StaticInput } from '../../../inputType'
|
||||
import { ClearableInput } from '../../../../common'
|
||||
|
||||
export let componentInput: StaticInput<string>
|
||||
export let value: string = '#fff'
|
||||
const dispatch = createEventDispatcher()
|
||||
let isOpen = false
|
||||
|
||||
$: dispatch('change', value)
|
||||
</script>
|
||||
|
||||
<div class="color-picker-input">
|
||||
<input
|
||||
type="text"
|
||||
readonly
|
||||
value={componentInput.value}
|
||||
on:focus|preventDefault|stopPropagation={() => (isOpen = true)}
|
||||
/>
|
||||
<div class="color-picker-input grow">
|
||||
<ClearableInput readonly bind:value on:focus={() => (isOpen = true)} />
|
||||
<ColorPicker
|
||||
bind:isOpen
|
||||
bind:hex={componentInput.value}
|
||||
label={componentInput.value}
|
||||
bind:hex={value}
|
||||
label={value}
|
||||
canChangeMode={false}
|
||||
components={ChromeVariant}
|
||||
toRight
|
||||
@@ -25,7 +24,7 @@
|
||||
</div>
|
||||
|
||||
<style global>
|
||||
.color-picker-input span > label.svelte-rogbpz {
|
||||
.color-picker-input span > label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -33,6 +32,7 @@
|
||||
box-shadow: 0 10px 40px -5px rgba(0, 0, 0, 0.25) !important;
|
||||
border-style: none !important;
|
||||
border-radius: 0.375rem !important;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.color-picker-input .slider-wrapper {
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@
|
||||
Inconsistent labeled resource object
|
||||
{/if}
|
||||
{:else if fieldType === 'color'}
|
||||
<ColorInput bind:componentInput />
|
||||
<ColorInput bind:value={componentInput.value} />
|
||||
{:else if fieldType === 'object'}
|
||||
{#if format?.startsWith('resource-')}
|
||||
<ResourcePicker
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition'
|
||||
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Badge, Button } from '../../../../common'
|
||||
import { secondaryMenu, SECONDARY_MENU_ID } from './'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppViewerContext } from '../../../types'
|
||||
|
||||
const { selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
let width: number
|
||||
let lastSelected = $selectedComponent
|
||||
|
||||
$: if (lastSelected !== $selectedComponent) {
|
||||
secondaryMenu.close()
|
||||
lastSelected = $selectedComponent
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:clientWidth={width}
|
||||
class="absolute z-50 inset-0 overflow-hidden"
|
||||
class:pointer-events-none={!$secondaryMenu.isOpen}
|
||||
>
|
||||
{#if $secondaryMenu.isOpen && $secondaryMenu.component}
|
||||
<div
|
||||
transition:fly|local={{ duration: 300, x: width, y: 0, opacity: 1 }}
|
||||
id={SECONDARY_MENU_ID}
|
||||
class="flex flex-col w-full h-full bg-white"
|
||||
>
|
||||
<div class="flex justify-between items-center bg-white gap-1 p-3">
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faChevronLeft }}
|
||||
on:click={secondaryMenu.close}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Badge color="blue">{$selectedComponent}</Badge>
|
||||
</div>
|
||||
<div class="relative h-full overflow-y-auto px-3 pb-3">
|
||||
{#if typeof $secondaryMenu.component === 'string'}
|
||||
{@html $secondaryMenu.component}
|
||||
{:else}
|
||||
<svelte:component this={$secondaryMenu.component} {...$secondaryMenu.props} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as SecondaryMenu } from './SecondaryMenu.svelte'
|
||||
export * from './menuStore'
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { SvelteComponent } from 'svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
export const SECONDARY_MENU_ID = 'app-secondary-menu' as const
|
||||
|
||||
export interface SecondaryMenuStore {
|
||||
isOpen: boolean
|
||||
component?: typeof SvelteComponent | string
|
||||
props: Record<string, any>
|
||||
}
|
||||
|
||||
const store = writable<SecondaryMenuStore>({ isOpen: false, component: undefined, props: {} })
|
||||
|
||||
export const secondaryMenu = {
|
||||
subscribe: store.subscribe,
|
||||
open: (component: SecondaryMenuStore['component'], props: SecondaryMenuStore['props'] = {}) => {
|
||||
store.set({ isOpen: true, component, props })
|
||||
},
|
||||
close: () => store.set({ isOpen: false, component: undefined, props: {} })
|
||||
} as const
|
||||
@@ -4,6 +4,7 @@ import type { History } from '$lib/history'
|
||||
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type { AppComponent, components } from './editor/component/components'
|
||||
import type { StyleProperty, StylePropertyKey } from './editor/componentsPanel/quickStyleProperties'
|
||||
import type {
|
||||
AppInput,
|
||||
ConnectedAppInput,
|
||||
@@ -99,6 +100,8 @@ export type InlineScript = {
|
||||
refreshOn?: { id: string; key: string }[]
|
||||
}
|
||||
|
||||
export type AppCssItemName = 'viewer' | 'grid' | AppComponent['type']
|
||||
|
||||
export type App = {
|
||||
grid: GridItem[]
|
||||
fullscreen: boolean
|
||||
@@ -114,9 +117,7 @@ export type App = {
|
||||
autoRefresh?: boolean
|
||||
doNotRecomputeOnInputChanged?: boolean
|
||||
}>
|
||||
css?: Partial<
|
||||
Record<'viewer' | 'grid' | AppComponent['type'], Record<string, ComponentCssProperty>>
|
||||
>
|
||||
css?: Partial<Record<AppCssItemName, Record<string, ComponentCssProperty>>>
|
||||
subgrids?: Record<string, GridItem[]>
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
export let variant: ButtonType.Variant = 'contained'
|
||||
export let mainClasses: string = ''
|
||||
export let toggleClasses: string = ''
|
||||
export let listClasses: string = ''
|
||||
export let disabled: boolean = false
|
||||
export let href: string | undefined = undefined
|
||||
export let target: ButtonType.Target = '_self'
|
||||
@@ -64,14 +65,16 @@
|
||||
{#if ref}
|
||||
<Popup
|
||||
{ref}
|
||||
let:open
|
||||
let:close
|
||||
options={{
|
||||
placement: $$slots.main ? 'bottom-end' : 'bottom',
|
||||
strategy: 'absolute',
|
||||
modifiers: [{ name: 'offset', options: { offset: [0, 0] } }]
|
||||
}}
|
||||
>
|
||||
<ul class="bg-white rounded-t border pt-1 pb-2 max-h-40 overflow-auto">
|
||||
<slot />
|
||||
<ul class="bg-white rounded-t border pt-1 pb-2 max-h-40 overflow-auto {listClasses}">
|
||||
<slot {open} {close} />
|
||||
</ul>
|
||||
</Popup>
|
||||
{/if}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
export let iconOnly: boolean = false
|
||||
export let startIcon: ButtonType.Icon | undefined = undefined
|
||||
export let endIcon: ButtonType.Icon | undefined = undefined
|
||||
export let wrapperClasses = ''
|
||||
|
||||
const props = getContext<ButtonType.ItemProps | undefined>(ButtonType.ItemContextKey)
|
||||
const iconWidthClass: Record<ButtonType.Size, string> = {
|
||||
@@ -45,7 +46,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<li class="mt-1">
|
||||
<li class="mt-1 {wrapperClasses}">
|
||||
<Button {...buttonProps} on:click>
|
||||
<slot />
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { X } from 'lucide-svelte'
|
||||
|
||||
export let value: any = ''
|
||||
export let placeholder = ''
|
||||
export let type: 'text' | 'textarea' | 'number' = 'text'
|
||||
export let inputClass = ''
|
||||
export let wrapperClass = ''
|
||||
export let buttonClass = ''
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: isNumeric = ['number', 'range'].includes(type)
|
||||
$: dispatch('change', value)
|
||||
|
||||
function handleInput(e) {
|
||||
value = isNumeric ? +e.target.value : e.target.value
|
||||
}
|
||||
|
||||
function clear() {
|
||||
value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative grow {wrapperClass}">
|
||||
{#if type === 'textarea'}
|
||||
<textarea
|
||||
{value}
|
||||
{placeholder}
|
||||
rows="1"
|
||||
class="resize-y duration-200 {inputClass}"
|
||||
{...$$restProps}
|
||||
on:input={handleInput}
|
||||
on:focus
|
||||
on:blur
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
{type}
|
||||
{value}
|
||||
{placeholder}
|
||||
class="duration-200 {(type === 'number' && value ? '!pr-[26px] ' : '') + inputClass}"
|
||||
{...$$restProps}
|
||||
on:input={handleInput}
|
||||
on:focus
|
||||
on:blur
|
||||
/>
|
||||
{/if}
|
||||
{#if value}
|
||||
<button
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
class="absolute z-10 top-1.5 right-1 rounded-full p-1 bg-white/60 duration-200 hover:bg-gray-200 {buttonClass}"
|
||||
aria-label="Clear"
|
||||
on:click|preventDefault|stopPropagation={clear}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
@@ -5,6 +5,7 @@ export { default as Button } from './button/Button.svelte'
|
||||
export { default as ButtonPopup } from './button/ButtonPopup.svelte'
|
||||
export { default as ButtonPopupItem } from './button/ButtonPopupItem.svelte'
|
||||
export { default as UndoRedo } from './button/UndoRedo.svelte'
|
||||
export { default as ClearableInput } from './clearableInput/ClearableInput.svelte'
|
||||
export { default as Drawer } from './drawer/Drawer.svelte'
|
||||
export { default as DrawerContent } from './drawer/DrawerContent.svelte'
|
||||
export { default as Kbd } from './kbd/Kbd.svelte'
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script lang="ts">
|
||||
export let kbdClass = ''
|
||||
</script>
|
||||
|
||||
<span
|
||||
class="{$$props.class} rounded border bg-white/70 px-1.5 !text-xs text-gray-600 shadow-sm font-light transition-all group-hover:border-primary-500 group-hover:text-primary-500"
|
||||
>
|
||||
<kbd>
|
||||
<kbd class={kbdClass}>
|
||||
<slot />
|
||||
</kbd>
|
||||
</span>
|
||||
|
||||
@@ -5,9 +5,13 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import { onMount } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
|
||||
export let noMinW = false
|
||||
export let show = false
|
||||
export let wrapperClasses = ''
|
||||
export let popupClasses = ''
|
||||
export let transitionDuration = 100
|
||||
let menu: HTMLDivElement
|
||||
|
||||
type Alignment = 'start' | 'end' | 'center'
|
||||
@@ -53,7 +57,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative" bind:this={menu}>
|
||||
<div class="relative {wrapperClasses}" bind:this={menu}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
on:click|stopPropagation={() => {
|
||||
@@ -69,10 +73,12 @@
|
||||
</div>
|
||||
{#if show}
|
||||
<div
|
||||
transition:fade|local={{ duration: transitionDuration }}
|
||||
class={classNames(
|
||||
'z-50 absolute mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none',
|
||||
placementsClasses[placement],
|
||||
noMinW ? 'min-w-0' : 'w-60'
|
||||
noMinW ? 'min-w-0' : 'w-60',
|
||||
popupClasses
|
||||
)}
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
|
||||
@@ -713,3 +713,7 @@ export function isMac(): boolean {
|
||||
export function getModifierKey(): string {
|
||||
return isMac() ? '⌘' : 'CTRL'
|
||||
}
|
||||
|
||||
export function isValidHexColor(color: string): boolean {
|
||||
return /^#(([A-F0-9]{2}){3,4}|[A-F0-9]{3})$/i.test(color)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user