Skip to content
2 changes: 2 additions & 0 deletions frontend/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ declare module 'vue' {
Files: typeof import('./src/components/Icons/Files.vue')['default']
FitScreen: typeof import('./src/components/Icons/FitScreen.vue')['default']
FontUploader: typeof import('./src/components/Controls/FontUploader.vue')['default']
GapControl: typeof import('./src/components/GapControl.vue')['default']
GlobalAI: typeof import('./src/components/Settings/GlobalAI.vue')['default']
GlobalAnalytics: typeof import('./src/components/Settings/GlobalAnalytics.vue')['default']
GlobalCode: typeof import('./src/components/Settings/GlobalCode.vue')['default']
Expand Down Expand Up @@ -141,6 +142,7 @@ declare module 'vue' {
SpacingControl: typeof import('./src/components/SpacingControl.vue')['default']
SplitInput: typeof import('./src/components/Controls/SplitInput.vue')['default']
SplitModeInput: typeof import('./src/components/Controls/SplitModeInput.vue')['default']
SplitPropertyControl: typeof import('./src/components/Controls/SplitPropertyControl.vue')['default']
StringOptions: typeof import('./src/components/PropsOptions/StringOptions.vue')['default']
StylePropertyControl: typeof import('./src/components/Controls/StylePropertyControl.vue')['default']
TabButtons: typeof import('./src/components/Controls/TabButtons.vue')['default']
Expand Down
9 changes: 2 additions & 7 deletions frontend/src/components/BlockFlexLayoutHandler.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,7 @@
{ label: 'Space Around', value: 'space-around' },
{ label: 'Space Evenly', value: 'space-evenly' },
]" />
<StylePropertyControl
v-if="blockController.isFlex()"
label="Gap"
propertyKey="gap"
:enableSlider="true"
:unitOptions="BOX_UNIT_OPTIONS" />
<GapControl v-if="blockController.isFlex()" />
<StylePropertyControl
propertyKey="flexWrap"
:component="OptionToggle"
Expand Down Expand Up @@ -73,7 +68,7 @@
<script lang="ts" setup>
import OptionToggle from "@/components/Controls/OptionToggle.vue";
import StylePropertyControl from "@/components/Controls/StylePropertyControl.vue";
import GapControl from "@/components/GapControl.vue";
import blockController from "@/utils/blockController";
import { BOX_UNIT_OPTIONS } from "@/utils/unitOptions";
import PlacementControl from "./PlacementControl.vue";
</script>
10 changes: 3 additions & 7 deletions frontend/src/components/BlockGridLayoutHandler.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,7 @@
type="text"
:modelValue="height"
@update:modelValue="setHeight" />
<StylePropertyControl
propertyKey="gap"
label="Gap"
v-if="blockController.isGrid()"
:enableSlider="true"
:unitOptions="BOX_UNIT_OPTIONS" />
<GapControl v-if="blockController.isGrid()" />
<!-- <InlineInput
label="Align"
v-if="blockController.isGrid()"
Expand Down Expand Up @@ -216,8 +211,9 @@
import InlineInput from "@/components/Controls/InlineInput.vue";
import OptionToggle from "@/components/Controls/OptionToggle.vue";
import StylePropertyControl from "@/components/Controls/StylePropertyControl.vue";
import GapControl from "@/components/GapControl.vue";
import blockController from "@/utils/blockController";
import { BOX_UNIT_OPTIONS, GRID_UNIT_OPTIONS } from "@/utils/unitOptions";
import { GRID_UNIT_OPTIONS } from "@/utils/unitOptions";
import { computed } from "vue";

const getGridType = () => {
Expand Down
42 changes: 5 additions & 37 deletions frontend/src/components/BorderControl.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,14 @@
:events="colorEvents" />

<!-- Border Width (with Split Mode Input) -->
<StylePropertyControl
<SplitPropertyControl
v-if="hasColor"
label="Border Width"
propertyKey="borderWidth"
:component="SplitModeInput"
:unitOptions="BORDER_UNIT_OPTIONS"
:enableStates="true"
:enableSlider="true"
:splits="SPLITS"
:toControlValues="toControlValues"
:toModelValue="toModelValue"
:normalizeValue="normalize"
:inputAttrs="{ min: 0 }"
:getModelValue="readValue"
:getVariantValue="readValue"
:getControlAttrs="getControlAttrs"
:getMergedValue="getMergedValue" />
:getModelValue="readValue" />

<!-- Border Style -->
<StylePropertyControl
Expand All @@ -42,45 +33,22 @@
</template>

<script lang="ts" setup>
import SplitModeInput from "@/components/Controls/SplitModeInput.vue";
import SplitPropertyControl from "@/components/Controls/SplitPropertyControl.vue";
import StylePropertyControl from "@/components/Controls/StylePropertyControl.vue";
import ColorInput from "@/components/Controls/ColorInput.vue";
import blockController from "@/utils/blockController";
import { expandBoxShorthand, normalizeValueWithUnits } from "@/utils/cssUtils";
import { BORDER_UNIT_OPTIONS } from "@/utils/unitOptions";
import { computed, ref, watch } from "vue";

type BoxValue = string | number | boolean | null;
import { computed } from "vue";

const SPLITS = ["T", "R", "B", "L"];
const splitModes = ref<Record<string, boolean>>({});

// Reset split modes on selection change
watch(
() => blockController.getSelectedBlocks(),
() => {
splitModes.value = {};
}
);

const readValue = (state: string | null = null) => {
const key = state ? `${state}:borderWidth` : "borderWidth";
return String(blockController.getStyle(key) || "");
};

const toControlValues = (value: unknown) => expandBoxShorthand(value);
const normalize = (value: BoxValue) => normalizeValueWithUnits(String(value || "0"), "px");
const toModelValue = (parts: BoxValue[]) => parts.join(" ");
const getMergedValue = (parts: BoxValue[]) => parts[0] ?? "0px";

const getControlAttrs = (variant: string | null) => {
const key = variant ?? "main";
return {
split: new Set(toControlValues(readValue(variant))).size > 1 || (splitModes.value[key] ?? false),
enableSlider: true,
"onUpdate:split": (split: boolean) => (splitModes.value[key] = split),
};
};
const toModelValue = (parts: StyleValue[]) => parts.join(" ");

const hasColor = computed(() => {
return Boolean(
Expand Down
41 changes: 3 additions & 38 deletions frontend/src/components/BorderRadiusControl.vue
Original file line number Diff line number Diff line change
@@ -1,64 +1,29 @@
<template>
<div class="flex w-full flex-col gap-2">
<StylePropertyControl
<SplitPropertyControl
label="Radius"
placeholder="None"
propertyKey="borderRadius"
:component="SplitModeInput"
:unitOptions="RADIUS_UNIT_OPTIONS"
:enableStates="true"
:enableSlider="true"
:splits="SPLITS"
:toControlValues
:toModelValue
:normalizeValue="normalize"
:inputAttrs="{ min: 0 }"
:getModelValue="readValue"
:getVariantValue="readValue"
:getControlAttrs="getControlAttrs"
:getMergedValue
@update:modelValue="ensureRoundedContentIsClipped" />
</div>
</template>

<script lang="ts" setup>
import SplitModeInput from "@/components/Controls/SplitModeInput.vue";
import StylePropertyControl from "@/components/Controls/StylePropertyControl.vue";
import SplitPropertyControl from "@/components/Controls/SplitPropertyControl.vue";
import blockController from "@/utils/blockController";
import { collapseBoxShorthand, expandBoxShorthand, normalizeValueWithUnits } from "@/utils/cssUtils";
import { RADIUS_UNIT_OPTIONS } from "@/utils/unitOptions";
import { ref, watch } from "vue";

type BoxValue = string | number | boolean | null;

const SPLITS = ["TL", "TR", "BR", "BL"];

const splitModes = ref<Record<string, boolean>>({});

watch(
() => blockController.getSelectedBlocks(),
() => (splitModes.value = {}),
);

const readValue = (state: string | null = null) =>
String(blockController.getStyle(state ? `${state}:borderRadius` : "borderRadius") || "");

const ensureRoundedContentIsClipped = (value: BoxValue) => {
const ensureRoundedContentIsClipped = (value: StyleValue) => {
if (!value) return;
if (!blockController.getStyle("overflowX")) blockController.setStyle("overflowX", "hidden");
if (!blockController.getStyle("overflowY")) blockController.setStyle("overflowY", "hidden");
};

const toControlValues = (value: unknown) => expandBoxShorthand(value);
const normalize = (value: BoxValue) => normalizeValueWithUnits(String(value || "0"), "px");
const toModelValue = (parts: BoxValue[]) => collapseBoxShorthand(parts);
const getMergedValue = (parts: BoxValue[]) => parts[0] ?? "0px";
const getControlAttrs = (variant: string | null) => {
const key = variant ?? "main";
return {
enableSlider: true,
split: new Set(toControlValues(readValue(variant))).size > 1 || (splitModes.value[key] ?? false),
"onUpdate:split": (split: boolean) => (splitModes.value[key] = split),
};
};
</script>
33 changes: 20 additions & 13 deletions frontend/src/components/Controls/SplitModeInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@
<script lang="ts" setup>
import Input from "@/components/Controls/Input.vue";
import SplitInput from "@/components/Controls/SplitInput.vue";
import blockController from "@/utils/blockController";
import { expandBoxShorthand } from "@/utils/cssUtils";
import { TabButtons, type TabButton } from "frappe-ui";
import type { HTMLAttributes } from "vue";
import { computed, useAttrs } from "vue";
import { computed, ref, useAttrs, watch } from "vue";

defineOptions({ inheritAttrs: false });

Expand All @@ -49,7 +50,6 @@ const props = withDefaults(
defineProps<{
modelValue?: InputValue;
placeholder?: string | number | boolean;
split?: boolean;
uniformTitle?: string;
splitTitle?: string;
splitOptions?: SplitOption[];
Expand All @@ -60,26 +60,27 @@ const props = withDefaults(
inputAttrs?: InputAttrs;
enableSlider?: boolean;
type?: string;
getMergedValue: (values: InputValue[]) => InputValue;
getMergedValue?: (values: InputValue[]) => InputValue;
}>(),
{
modelValue: "",
placeholder: "",
split: false,
uniformTitle: "Use for all",
splitTitle: "Set separately",
splits: 0,
toControlValues: (value: unknown) => (Array.isArray(value) ? value : [value as InputValue]),
toModelValue: (values: InputValue[]) => values,
normalizeValue: (value: InputValue) => value,
inputAttrs: () => ({}),
enableSlider: false,
// The base control filters its own enableSlider out of the fallthrough attrs, so split
// inputs opt in here instead.
enableSlider: true,
getMergedValue: (values: InputValue[]) => values[0] ?? "0px",
},
);

const emit = defineEmits<{
"update:modelValue": [value: InputValue];
"update:split": [value: boolean];
}>();

const attrs = useAttrs();
Expand All @@ -90,8 +91,17 @@ const controlAttrs = computed(() =>

const splitCount = computed(() => (typeof props.splits === "number" ? props.splits : props.splits.length));
const splitValues = computed(() => props.toControlValues(props.modelValue, splitCount.value));

// Split mode turns itself on when the sides already differ; the toggle only forces it on.
const forceSplit = ref(false);
const split = computed(() => new Set(splitValues.value).size > 1 || forceSplit.value);
watch(
() => blockController.getSelectedBlocks(),
() => (forceSplit.value = false),
);

const displayValue = computed(() => {
if (!props.split) return props.modelValue;
if (!split.value) return props.modelValue;
if (String(props.modelValue ?? "").trim() === "Mixed") return "";
const values = expandBoxShorthand(props.modelValue);
return new Set(values).size === 1 ? values[0] : "";
Expand All @@ -116,18 +126,15 @@ const resolvedSplitOptions = computed(() => props.splitOptions ?? defaultSplitOp

const setSplitValue = (value: string | number | boolean | undefined) => {
if (typeof value !== "boolean") return;
if (!value) {
const mergedValue = props.getMergedValue(splitValues.value);
emit("update:modelValue", mergedValue);
}
emit("update:split", value);
if (!value) emit("update:modelValue", props.getMergedValue(splitValues.value));
forceSplit.value = value;
};

const setIndividualValue = (value: unknown) => emit("update:modelValue", value as InputValue);

const setUniformValue = (value: InputValue) => {
if (String(value ?? "") === String(displayValue.value ?? "")) return;
if (props.split) emit("update:split", false);
forceSplit.value = false;
emit("update:modelValue", value);
};
</script>
56 changes: 56 additions & 0 deletions frontend/src/components/Controls/SplitPropertyControl.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<template>
<StylePropertyControl v-bind="{ ...splitProps, ...$attrs }" :component="SplitModeInput">
<template v-for="(_, name) in $slots" :key="name" #[name]="slotData">
<slot :name="name" v-bind="slotData || {}" />
</template>
</StylePropertyControl>
</template>

<script lang="ts" setup>
import SplitModeInput from "@/components/Controls/SplitModeInput.vue";
import StylePropertyControl from "@/components/Controls/StylePropertyControl.vue";
import { collapseBoxShorthand, expandBoxShorthand, normalizeValueWithUnits } from "@/utils/cssUtils";
import { BOX_UNIT_OPTIONS } from "@/utils/unitOptions";
import { computed } from "vue";

defineOptions({ inheritAttrs: false });

type InputAttrs = Record<string, unknown>;
type SplitConfig = string | { label?: string; attrs?: InputAttrs };

const props = withDefaults(
defineProps<{
propertyKey: string;
splits: number | SplitConfig[];
unitOptions?: string[];
toControlValues?: (value: unknown, count: number) => StyleValue[];
toModelValue?: (values: StyleValue[], changedIndex: number) => unknown;
normalizeValue?: (value: StyleValue, index: number) => StyleValue;
inputAttrs?: InputAttrs;
// Reads the base value when called without a state, the variant value otherwise.
getModelValue?: (state?: string | null) => string | number | boolean;
getVariantValue?: (variant: string) => string | number | boolean;
}>(),
{
unitOptions: () => BOX_UNIT_OPTIONS,
toControlValues: (value: unknown) => expandBoxShorthand(value),
toModelValue: (values: StyleValue[]) => collapseBoxShorthand(values),
inputAttrs: () => ({ min: 0 }),
},
);

const defaultUnit = computed(() => props.unitOptions[0] ?? "px");

const normalizeValue = computed(
() =>
props.normalizeValue ??
((value: StyleValue) => normalizeValueWithUnits(String(value || "0"), defaultUnit.value)),
);

const splitProps = computed(() => ({
...props,
enableSlider: true,
normalizeValue: normalizeValue.value,
getVariantValue: props.getVariantValue ?? props.getModelValue,
}));
</script>
Loading
Loading