diff --git a/frontend/src/lib/components/apps/editor/componentsPanel/CssProperty.svelte b/frontend/src/lib/components/apps/editor/componentsPanel/CssProperty.svelte index f5f8cb35a8..c34a9c28d4 100644 --- a/frontend/src/lib/components/apps/editor/componentsPanel/CssProperty.svelte +++ b/frontend/src/lib/components/apps/editor/componentsPanel/CssProperty.svelte @@ -1,20 +1,21 @@
{addWhitespaceBeforeCapitals(name)}
{#if value} -
+
{#if value.style !== undefined || forceStyle}
@@ -54,7 +55,7 @@ color="light" size="xs" btnClasses="!p-1 !w-[34px] !h-[34px] {isQuickMenuOpen - ? '!bg-gray-200/60 hover:!bg-gray-200' + ? '!bg-gray-200/60 hover:!bg-gray-200 focus:!bg-gray-200' : ''}" aria-label="{isQuickMenuOpen ? 'Close' : 'Open'} styling menu" on:click={toggleQuickMenu} @@ -71,7 +72,12 @@ {#if quickStyleProperties?.length && isQuickMenuOpen}
- +
{/if}
diff --git a/frontend/src/lib/components/apps/editor/componentsPanel/ListItem.svelte b/frontend/src/lib/components/apps/editor/componentsPanel/ListItem.svelte index f6340580a6..58eb6cbf38 100644 --- a/frontend/src/lib/components/apps/editor/componentsPanel/ListItem.svelte +++ b/frontend/src/lib/components/apps/editor/componentsPanel/ListItem.svelte @@ -2,10 +2,15 @@ import { slide } from 'svelte/transition' import { ChevronDown } from 'lucide-svelte' import { isOpenStore } from './store' - import { createEventDispatcher } from 'svelte' + import { createEventDispatcher, onMount } from 'svelte' export let title: string export let prefix: string | undefined = undefined + export let openByDefault: boolean = false + export let wrapperClasses = '' + export let toggleClasses = '' + export let contentWrapperClasses = '' + export let isOpen = false const dispatch = createEventDispatcher() @@ -13,14 +18,20 @@ $: isOpen = prefix ? $isOpenStore[storeTitle] : true $: dispatch('open', isOpen) + + onMount(() => { + if (prefix !== undefined && !(prefix + title in $isOpenStore)) { + $isOpenStore[prefix + title] = openByDefault + } + }) -
+
{#if prefix !== undefined} {#if isOpen} -
+
{/if} diff --git a/frontend/src/lib/components/apps/editor/componentsPanel/QuickStyleMenu.svelte b/frontend/src/lib/components/apps/editor/componentsPanel/QuickStyleMenu.svelte index 6ef985f47c..5169e6f48f 100644 --- a/frontend/src/lib/components/apps/editor/componentsPanel/QuickStyleMenu.svelte +++ b/frontend/src/lib/components/apps/editor/componentsPanel/QuickStyleMenu.svelte @@ -8,18 +8,24 @@ StylePropertyType, StylePropertyUnits, STYLE_STORE_KEY, + type PropertyGroup, type StylePropertyKey, type TopColors } from './quickStyleProperties' import QuickStyleProperty from './QuickStyleProperty.svelte' + import ListItem from './ListItem.svelte' + import type { TypedComponent } from '../component' export let value = '' - export let properties: StylePropertyKey[] + export let properties: PropertyGroup[] + export let componentType: TypedComponent['type'] | undefined = undefined + export let componentProperty: string | undefined = undefined const { app } = getContext('AppViewerContext') const styleStore = createStyleStore(properties) setContext(STYLE_STORE_KEY, styleStore) let multiValues: Record = initiateMultiValues() let mounted = false + let isOpen: Record = {} $: mounted && $styleStore && writeStyle() $: mounted && (!value || value) && parseStyle() @@ -154,32 +160,70 @@ $styleStore.style[index].value = values.join(' ').trim() } + function formatKebabCase(text: string) { + text = text.toLowerCase().replace(/-/, ' ') + if (text.length) { + text = text[0].toUpperCase() + text.slice(1) + } + return text + } + onMount(() => { parseStyle() mounted = true }) -
- {#each $styleStore.style as { prop }, index} -
-
{prop.key}
-
- {#if Array.isArray(prop.value)} -
- {#each prop.value as value, i} - setMultiValueProperty(index)} - /> - {/each} -
- {:else} - - {/if} -
-
+
+ {#each properties as property, i} + {#each Object.keys(property) as group, j} + {@const prefix = `${componentType}_${componentProperty}_${group}`} + + + + {group} + + +
+ {#each property[group] as p} + {@const { + prop: { prop }, + index + } = styleStore.getProp(p)} + {#if prop !== undefined && index !== undefined} +
+
+ {formatKebabCase(prop.key)} +
+
+ {#if Array.isArray(prop.value)} +
+ {#each prop.value as value, i} + setMultiValueProperty(index)} + /> + {/each} +
+ {:else} + + {/if} +
+
+ {/if} + {/each} +
+
+ {/each} {/each}
diff --git a/frontend/src/lib/components/apps/editor/componentsPanel/QuickStyleProperty.svelte b/frontend/src/lib/components/apps/editor/componentsPanel/QuickStyleProperty.svelte index 34a548f874..3b607f62f4 100644 --- a/frontend/src/lib/components/apps/editor/componentsPanel/QuickStyleProperty.svelte +++ b/frontend/src/lib/components/apps/editor/componentsPanel/QuickStyleProperty.svelte @@ -60,6 +60,7 @@ {:else if type === StylePropertyType.number} properties.includes(p.key)).map((p) => ({ +export function createStyleStore(properties: PropertyGroup[]) { + const propertyNames = properties.reduce((acc, p) => { + Object.values(p).forEach((names) => acc.push(...names)) + return acc + }, [] as string[]) + const style = StyleProperty.filter((p) => propertyNames.includes(p.key)).map((p) => ({ prop: p, value: '' as string | undefined })) @@ -93,10 +98,45 @@ export const StylePropertyUnits = ['px', 'em', 'rem', '%', 'vh', 'vw'] export type TopColors = [] | [string] | [string, string] | [string, string, string] +export type StylePropertyOption = { + text: string + icon: string | typeof SvelteComponent +} + +export type BaseStylePropertyValue = { + type: T + title?: string +} + +export type StylePropertyColorValue = BaseStylePropertyValue + +export type StylePropertyUnitValue = BaseStylePropertyValue + +export type StylePropertyNumberValue = BaseStylePropertyValue & { + step?: number + min?: number + max?: number +} + +export type StylePropertyTextValue = BaseStylePropertyValue & { + options?: StylePropertyOption[] +} + +export type StylePropertyValue = + | StylePropertyColorValue + | StylePropertyUnitValue + | StylePropertyNumberValue + | StylePropertyTextValue + +export type StyleProperty = { + key: string + value: StylePropertyValue | StylePropertyValue[] +} + export type StylePropertyKey = (typeof StyleProperty)[number]['key'] // Using an array instead of an object to preserve the order of the properties -export const StyleProperty = [ +export const StyleProperty: StyleProperty[] = [ { key: 'display', value: { @@ -277,18 +317,18 @@ export const StyleProperty = [ type: StylePropertyType.color } }, - { - key: 'font-size', - value: { - type: StylePropertyType.unit - } - }, { key: 'font-family', value: { type: StylePropertyType.text } }, + { + key: 'font-size', + value: { + type: StylePropertyType.unit + } + }, { key: 'font-weight', value: { @@ -421,67 +461,91 @@ export const StyleProperty = [ ] } } -] 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' +export type PropertyGroup = Record + +// Property groups +const layoutGrouping: PropertyGroup = { + layout: ['display', 'overflow'] +} + +const sizeGrouping: PropertyGroup = { + size: ['width', 'min-width', 'max-width', 'height', 'min-height', 'max-height'] +} + +const spacingGrouping: PropertyGroup = { + spacing: ['padding'] +} + +const borderGrouping: PropertyGroup = { + borders: ['border', 'border-radius'] +} + +const typographyGrouping: PropertyGroup = { + typography: [ + 'color', + 'font-size', + 'font-family', + 'font-weight', + 'font-style', + 'text-align', + 'text-decoration', + 'text-transform', + 'line-height', + 'letter-spacing', + 'word-spacing' + ] +} + +const backgroundGrouping: PropertyGroup = { + background: ['background-color', 'opacity', 'box-shadow'] +} + +const miscGrouping: PropertyGroup = { + miscellaneous: ['cursor'] +} + +// Commonly used-together property groups +const containerDefaultProps: PropertyGroup[] = [ + layoutGrouping, + spacingGrouping, + backgroundGrouping, + borderGrouping ] -const sizeDefaultProps: StylePropertyKey[] = [ - 'width', - 'min-width', - 'max-width', - 'height', - 'min-height', - 'max-height' +const buttonDefaultProps: PropertyGroup[] = [ + backgroundGrouping, + borderGrouping, + typographyGrouping, + miscGrouping ] -const buttonDefaultProps: StylePropertyKey[] = [ - 'cursor', - 'border', - 'border-radius', - 'box-shadow', - ...textDefaultProps +const inputDefaultProps: PropertyGroup[] = [borderGrouping, typographyGrouping, miscGrouping] + +const sliderDefaultProps: PropertyGroup[] = [ + { + colors: ['color', 'background-color', 'opacity'] + }, + borderGrouping, + miscGrouping ] -const inputDefaultProps: StylePropertyKey[] = [ - 'cursor', - 'border', - 'border-radius', - ...textDefaultProps +const dividerDefaultProps: PropertyGroup[] = [ + { + colors: ['background-color', 'opacity'] + }, + sizeGrouping, + spacingGrouping, + borderGrouping ] export const quickStyleProperties: Record< keyof typeof components, - Record + Record > = { mapcomponent: { - map: containerDefaultProps + map: [spacingGrouping, borderGrouping] }, pdfcomponent: { container: containerDefaultProps @@ -491,40 +555,43 @@ export const quickStyleProperties: Record< button: buttonDefaultProps }, htmlcomponent: { - container: allDefaultProps + container: [ + layoutGrouping, + sizeGrouping, + spacingGrouping, + borderGrouping, + typographyGrouping, + backgroundGrouping + ] }, iconcomponent: { container: containerDefaultProps, icon: [ - 'padding', - 'opacity', - 'cursor', - 'width', - 'min-width', - 'max-width', - 'height', - 'min-height', - 'max-height', - 'color' + { + colors: ['color', 'background-color', 'opacity'] + }, + sizeGrouping, + spacingGrouping, + miscGrouping ] }, tabscomponent: { tabRow: containerDefaultProps, - allTabs: [...textDefaultProps, ...sizeDefaultProps], - selectedTab: [...textDefaultProps, ...sizeDefaultProps], + allTabs: [typographyGrouping, sizeGrouping], + selectedTab: [typographyGrouping, sizeGrouping], container: containerDefaultProps }, textcomponent: { - text: textDefaultProps + text: [typographyGrouping] }, imagecomponent: { image: containerDefaultProps }, rangecomponent: { - handles: ['opacity', 'cursor', 'border', 'border-radius', 'background-color'], - bar: ['opacity', 'cursor', 'border', 'border-radius', 'background-color'], - limits: textDefaultProps, - values: textDefaultProps + handles: sliderDefaultProps, + bar: sliderDefaultProps, + limits: [typographyGrouping], + values: [typographyGrouping] }, tablecomponent: { tableHeader: containerDefaultProps, @@ -544,20 +611,20 @@ export const quickStyleProperties: Record< input: inputDefaultProps }, slidercomponent: { - handles: ['opacity', 'cursor', 'border', 'border-radius', 'background-color'], - bar: ['opacity', 'cursor', 'border', 'border-radius', 'background-color'], - limits: textDefaultProps, - values: textDefaultProps + handles: sliderDefaultProps, + bar: sliderDefaultProps, + limits: [typographyGrouping], + values: [typographyGrouping] }, displaycomponent: { - header: [...containerDefaultProps, ...textDefaultProps], + header: [...containerDefaultProps, typographyGrouping], container: containerDefaultProps }, barchartcomponent: { container: containerDefaultProps }, checkboxcomponent: { - text: textDefaultProps + text: [typographyGrouping] }, currencycomponent: { input: inputDefaultProps @@ -607,35 +674,11 @@ export const quickStyleProperties: Record< input: inputDefaultProps }, verticaldividercomponent: { - divider: [ - 'padding', - 'opacity', - 'width', - 'min-width', - 'max-width', - 'height', - 'min-height', - 'max-height', - 'border', - 'border-radius', - 'background-color' - ], + divider: dividerDefaultProps, container: containerDefaultProps }, horizontaldividercomponent: { - divider: [ - 'padding', - 'opacity', - 'width', - 'min-width', - 'max-width', - 'height', - 'min-height', - 'max-height', - 'border', - 'border-radius', - 'background-color' - ], + divider: dividerDefaultProps, container: containerDefaultProps }, verticalsplitpanescomponent: { diff --git a/frontend/src/lib/components/apps/editor/componentsPanel/store.ts b/frontend/src/lib/components/apps/editor/componentsPanel/store.ts index 84fbeef5e4..1d8de4ffd6 100644 --- a/frontend/src/lib/components/apps/editor/componentsPanel/store.ts +++ b/frontend/src/lib/components/apps/editor/componentsPanel/store.ts @@ -7,6 +7,7 @@ export type IsOpenStoreItem = Record export const isOpenStore = { subscribe: store.subscribe, update: store.update, + set: store.set, /** If an item is already set, it won't get updated. */ addItems: (items: IsOpenStoreItem[]) => { let newItems = {} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/StylePanel.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/StylePanel.svelte index ea8e624af9..aa04934a61 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/StylePanel.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/StylePanel.svelte @@ -47,6 +47,7 @@ color="light" size="xs" aria-label="Apply to all instances of this component" + btnClasses="ml-3 mt-2" on:click={applyToAllInstances} > Copy style to global CSS   @@ -60,6 +61,7 @@ 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)} /> diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/ColorInput.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/ColorInput.svelte index 1186a2d99a..7ad1166b2a 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/ColorInput.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/ColorInput.svelte @@ -32,7 +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; + z-index: 30; } .color-picker-input .slider-wrapper { diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/secondaryMenu/SecondaryMenu.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/secondaryMenu/SecondaryMenu.svelte index e010f8263a..8adcf9f795 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/secondaryMenu/SecondaryMenu.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/secondaryMenu/SecondaryMenu.svelte @@ -27,7 +27,7 @@ id={SECONDARY_MENU_ID} class="flex flex-col w-full h-full bg-white" > -
+
-
+
{#if typeof $secondaryMenu.component === 'string'} {@html $secondaryMenu.component} {:else}